Skip to content

Commit

Permalink
Update SQLAlchemy example(s) to use txn retries
Browse files Browse the repository at this point in the history
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
rmloveland committed Jan 8, 2019
1 parent 9d1b831 commit f0decbc
Show file tree
Hide file tree
Showing 8 changed files with 675 additions and 215 deletions.
35 changes: 0 additions & 35 deletions _includes/v2.1/app/insecure/sqlalchemy-basic-sample.py

This file was deleted.

116 changes: 94 additions & 22 deletions _includes/v2.1/app/sqlalchemy-basic-sample.py
@@ -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)
60 changes: 60 additions & 0 deletions _includes/v2.1/app/sqlalchemy-large-txns.py
@@ -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)
35 changes: 0 additions & 35 deletions _includes/v2.2/app/insecure/sqlalchemy-basic-sample.py

This file was deleted.

116 changes: 94 additions & 22 deletions _includes/v2.2/app/sqlalchemy-basic-sample.py
@@ -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)

0 comments on commit f0decbc

Please sign in to comment.