Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update SQLAlchemy example(s) to use txn retries
Addresses #3315. Summary of changes: - Update SQLAlchemy example code to use `run_transaction` function to perform txn retries automagically. - Address the issue that the previous code sample could not be run more than once. - Add note that Python 3 is used for the example code - Add new 'Best Practices' section that describes what to do (and not do) when driving CockroachDB from SQLAlchemy, including: - Noting that one should not munge txn state inside `run_transaction`. - Showing some example code to address "transaction too large errors" by breaking updates into chunks. - Recommendation to use IMPORT for bulk work, not ORMs. - Recommendation to prefer query builder over ORM, if possible. - Note: All of the changes listed above apply to v2.1 and v2.2 docs.
- Loading branch information
1 parent
9d1b831
commit f0decbc
Showing
8 changed files
with
675 additions
and
215 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,110 @@ | ||
from __future__ import print_function | ||
import random | ||
from math import floor | ||
from sqlalchemy import create_engine, Column, Integer | ||
from sqlalchemy.ext.declarative import declarative_base | ||
from sqlalchemy.orm import sessionmaker | ||
from cockroachdb.sqlalchemy import run_transaction | ||
|
||
Base = declarative_base() | ||
|
||
|
||
# The Account class corresponds to the "accounts" database table. | ||
class Account(Base): | ||
__tablename__ = 'accounts' | ||
id = Column(Integer, primary_key=True) | ||
balance = Column(Integer) | ||
|
||
# Create an engine to communicate with the database. The "cockroachdb://" prefix | ||
# for the engine URL indicates that we are connecting to CockroachDB. | ||
engine = create_engine('cockroachdb://maxroach@localhost:26257/bank', | ||
connect_args = { | ||
'sslmode' : 'require', | ||
'sslrootcert': 'certs/ca.crt', | ||
'sslkey':'certs/client.maxroach.key', | ||
'sslcert':'certs/client.maxroach.crt' | ||
}) | ||
Session = sessionmaker(bind=engine) | ||
|
||
# Create an engine to communicate with the database. The | ||
# "cockroachdb://" prefix for the engine URL indicates that we are | ||
# connecting to CockroachDB using the 'cockroachdb' dialect. | ||
# For more information, see | ||
# https://github.com/cockroachdb/cockroachdb-python. | ||
|
||
secure_cluster = True # Set to False for insecure clusters | ||
connect_args = {} | ||
|
||
if secure_cluster: | ||
connect_args = { | ||
'sslmode': 'require', | ||
'sslrootcert': 'certs/ca.crt', | ||
'sslkey': 'certs/client.maxroach.key', | ||
'sslcert': 'certs/client.maxroach.crt' | ||
} | ||
else: | ||
connect_args = {'sslmode': 'disable'} | ||
|
||
engine = create_engine( | ||
'cockroachdb://maxroach@localhost:26257/bank', | ||
connect_args=connect_args, | ||
echo=True # Log SQL queries to stdout | ||
) | ||
|
||
# Automatically create the "accounts" table based on the Account class. | ||
Base.metadata.create_all(engine) | ||
|
||
# Insert two rows into the "accounts" table. | ||
session = Session() | ||
session.add_all([ | ||
Account(id=1, balance=1000), | ||
Account(id=2, balance=250), | ||
]) | ||
session.commit() | ||
|
||
# Print out the balances. | ||
for account in session.query(Account): | ||
print(account.id, account.balance) | ||
|
||
# Store the account IDs we create for later use. | ||
|
||
seen_account_ids = set() | ||
|
||
|
||
# The code below generates random IDs for new accounts. | ||
|
||
def create_random_accounts(sess, n): | ||
"""Create N new accounts with random IDs and random account balances. | ||
Note that since this is a demo, we don't do any work to ensure the | ||
new IDs don't collide with existing IDs. | ||
""" | ||
new_accounts = [] | ||
elems = iter(range(n)) | ||
for i in elems: | ||
billion = 1000000000 | ||
new_id = floor(random.random()*billion) | ||
seen_account_ids.add(new_id) | ||
new_accounts.append( | ||
Account( | ||
id=new_id, | ||
balance=floor(random.random()*1000000) | ||
) | ||
) | ||
sess.add_all(new_accounts) | ||
|
||
|
||
run_transaction(sessionmaker(bind=engine), | ||
lambda s: create_random_accounts(s, 100)) | ||
|
||
|
||
# Helper for getting random existing account IDs. | ||
|
||
def get_random_account_id(): | ||
id = random.choice(tuple(seen_account_ids)) | ||
return id | ||
|
||
|
||
def transfer_funds_randomly(session): | ||
"""Transfer money randomly between accounts (during SESSION). | ||
Cuts a randomly selected account's balance in half, and gives the | ||
other half to some other randomly selected account. | ||
""" | ||
source_id = get_random_account_id() | ||
sink_id = get_random_account_id() | ||
|
||
source = session.query(Account).filter_by(id=source_id).one() | ||
amount = floor(source.balance/2) | ||
|
||
# Check balance of the first account. | ||
if source.balance < amount: | ||
raise "Insufficient funds" | ||
|
||
source.balance -= amount | ||
session.query(Account).filter_by(id=sink_id).update( | ||
{"balance": (Account.balance + amount)} | ||
) | ||
|
||
|
||
# Run the transfer inside a transaction. | ||
|
||
run_transaction(sessionmaker(bind=engine), transfer_funds_randomly) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
from sqlalchemy import create_engine, Column, Float, Integer | ||
from sqlalchemy.ext.declarative import declarative_base | ||
from sqlalchemy.orm import sessionmaker | ||
from cockroachdb.sqlalchemy import run_transaction | ||
from random import random | ||
|
||
Base = declarative_base() | ||
|
||
# The code below assumes you are running as 'root' and have run | ||
# the following SQL statements against an insecure cluster. | ||
|
||
# CREATE DATABASE pointstore; | ||
|
||
# USE pointstore; | ||
|
||
# CREATE TABLE points ( | ||
# id INT PRIMARY KEY DEFAULT unique_rowid(), | ||
# x FLOAT NOT NULL, | ||
# y FLOAT NOT NULL, | ||
# z FLOAT NOT NULL | ||
# ); | ||
|
||
engine = create_engine( | ||
'cockroachdb://root@localhost:26257/pointstore', | ||
connect_args={ | ||
'sslmode': 'disable', | ||
}, | ||
echo=True | ||
) | ||
|
||
|
||
class Point(Base): | ||
__tablename__ = 'points' | ||
id = Column(Integer, primary_key=True) | ||
x = Column(Float) | ||
y = Column(Float) | ||
z = Column(Float) | ||
|
||
|
||
def add_points(num_points): | ||
chunk_size = 1000 # Tune this based on object sizes. | ||
|
||
def add_points_helper(sess, chunk, num_points): | ||
points = [] | ||
for i in range(chunk, min(chunk + chunk_size, num_points)): | ||
points.append( | ||
Point(x=random()*1024, y=random()*1024, z=random()*1024) | ||
) | ||
sess.bulk_save_objects(points) | ||
|
||
for chunk in range(0, num_points, chunk_size): | ||
run_transaction( | ||
sessionmaker(bind=engine), | ||
lambda s: add_points_helper( | ||
s, chunk, min(chunk + chunk_size, num_points) | ||
) | ||
) | ||
|
||
|
||
add_points(10000) |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,110 @@ | ||
from __future__ import print_function | ||
import random | ||
from math import floor | ||
from sqlalchemy import create_engine, Column, Integer | ||
from sqlalchemy.ext.declarative import declarative_base | ||
from sqlalchemy.orm import sessionmaker | ||
from cockroachdb.sqlalchemy import run_transaction | ||
|
||
Base = declarative_base() | ||
|
||
|
||
# The Account class corresponds to the "accounts" database table. | ||
class Account(Base): | ||
__tablename__ = 'accounts' | ||
id = Column(Integer, primary_key=True) | ||
balance = Column(Integer) | ||
|
||
# Create an engine to communicate with the database. The "cockroachdb://" prefix | ||
# for the engine URL indicates that we are connecting to CockroachDB. | ||
engine = create_engine('cockroachdb://maxroach@localhost:26257/bank', | ||
connect_args = { | ||
'sslmode' : 'require', | ||
'sslrootcert': 'certs/ca.crt', | ||
'sslkey':'certs/client.maxroach.key', | ||
'sslcert':'certs/client.maxroach.crt' | ||
}) | ||
Session = sessionmaker(bind=engine) | ||
|
||
# Create an engine to communicate with the database. The | ||
# "cockroachdb://" prefix for the engine URL indicates that we are | ||
# connecting to CockroachDB using the 'cockroachdb' dialect. | ||
# For more information, see | ||
# https://github.com/cockroachdb/cockroachdb-python. | ||
|
||
secure_cluster = True # Set to False for insecure clusters | ||
connect_args = {} | ||
|
||
if secure_cluster: | ||
connect_args = { | ||
'sslmode': 'require', | ||
'sslrootcert': 'certs/ca.crt', | ||
'sslkey': 'certs/client.maxroach.key', | ||
'sslcert': 'certs/client.maxroach.crt' | ||
} | ||
else: | ||
connect_args = {'sslmode': 'disable'} | ||
|
||
engine = create_engine( | ||
'cockroachdb://maxroach@localhost:26257/bank', | ||
connect_args=connect_args, | ||
echo=True # Log SQL queries to stdout | ||
) | ||
|
||
# Automatically create the "accounts" table based on the Account class. | ||
Base.metadata.create_all(engine) | ||
|
||
# Insert two rows into the "accounts" table. | ||
session = Session() | ||
session.add_all([ | ||
Account(id=1, balance=1000), | ||
Account(id=2, balance=250), | ||
]) | ||
session.commit() | ||
|
||
# Print out the balances. | ||
for account in session.query(Account): | ||
print(account.id, account.balance) | ||
|
||
# Store the account IDs we create for later use. | ||
|
||
seen_account_ids = set() | ||
|
||
|
||
# The code below generates random IDs for new accounts. | ||
|
||
def create_random_accounts(sess, n): | ||
"""Create N new accounts with random IDs and random account balances. | ||
Note that since this is a demo, we don't do any work to ensure the | ||
new IDs don't collide with existing IDs. | ||
""" | ||
new_accounts = [] | ||
elems = iter(range(n)) | ||
for i in elems: | ||
billion = 1000000000 | ||
new_id = floor(random.random()*billion) | ||
seen_account_ids.add(new_id) | ||
new_accounts.append( | ||
Account( | ||
id=new_id, | ||
balance=floor(random.random()*1000000) | ||
) | ||
) | ||
sess.add_all(new_accounts) | ||
|
||
|
||
run_transaction(sessionmaker(bind=engine), | ||
lambda s: create_random_accounts(s, 100)) | ||
|
||
|
||
# Helper for getting random existing account IDs. | ||
|
||
def get_random_account_id(): | ||
id = random.choice(tuple(seen_account_ids)) | ||
return id | ||
|
||
|
||
def transfer_funds_randomly(session): | ||
"""Transfer money randomly between accounts (during SESSION). | ||
Cuts a randomly selected account's balance in half, and gives the | ||
other half to some other randomly selected account. | ||
""" | ||
source_id = get_random_account_id() | ||
sink_id = get_random_account_id() | ||
|
||
source = session.query(Account).filter_by(id=source_id).one() | ||
amount = floor(source.balance/2) | ||
|
||
# Check balance of the first account. | ||
if source.balance < amount: | ||
raise "Insufficient funds" | ||
|
||
source.balance -= amount | ||
session.query(Account).filter_by(id=sink_id).update( | ||
{"balance": (Account.balance + amount)} | ||
) | ||
|
||
|
||
# Run the transfer inside a transaction. | ||
|
||
run_transaction(sessionmaker(bind=engine), transfer_funds_randomly) |
Oops, something went wrong.