Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SQLAlchemy Session - In Depth by Mike Bayer

PyCon 2013
March 17, 2013
2k

SQLAlchemy Session - In Depth by Mike Bayer

PyCon 2013

March 17, 2013
Tweet

Transcript

  1. The Transaction • The  primary  system  employed  by  relational  

    databases  for  managing  data. • Provides  a  scope  around  a  series  of  operations  with   lots  of  desirable  behaviors. • The  transaction  follows  the  ACID  model. • Relational  databases  usually  use  transactions  for  all   operations;  if  they  aren't  apparent,  it  is  probably   using  "autocommit"  by  default.
  2. Transactions  are  atomic  -­‐  all  changes  which  occur  can  be

      rolled  back  to  the  state  preceding  the  transaction. ACID Model address [email protected] 1 1 address [email protected] 1 [email protected] 1 1 2 address [email protected] 1 [email protected] 1 [email protected] 2 1 2 3 ROLLBACK INSERT INTO address (id, user_id, email) VALUES (2, 1, '[email protected]') INSERT INTO address (id, user_id, email) VALUES (3, 2, '[email protected]') BEGIN TRANSACTION DATABASE STATE TRANSACTION address [email protected] 1 1
  3. The  transaction  provides  consistency;  rules  exist  for  how   data

     can  be  created  and  manipulated,  which  often  limit   the  order  in  which  operations  can  take  place ACID Model address [email protected] 1 1 address [email protected] 1 [email protected] 1 1 2 INSERT INTO address (id, user_id, email) VALUES (2, 1, '[email protected]') Constraints: 1. NOT NULL fields present 2. primary key unique user Ed Jones 1 INSERT INTO user (id, name) VALUES (1, 'Ed Jones') INSERT INTO address (id, user_id, email) VALUES (1, 1, '[email protected]') Constraints: 1. NOT NULL fields all present 2. primary key unique 3. user_id column present in user.id TRANSACTION Constraints: 1. NOT NULL fields all present 2. primary key unique 3. user_id column present in user.id
  4. Transactions  are  isolated  -­‐  to  a  varying  degree,  changes  

    on  the  inside  aren't  visible  on  the  outside,  and  vice  versa.     Historically,  table  and  row  locks  are  used  to  achieve  this... ACID Model address [email protected] 1 [email protected] 1 [email protected] 2 1 2 3 address [email protected] 1 1 address [email protected] 1 [email protected] 1 1 2 address [email protected] 1 [email protected] 1 [email protected] 2 1 2 3 UPDATE address SET email='[email protected]' WHERE id=2 INSERT INTO address (id, user_id, email) VALUES (3, 2, '[email protected]') DATABASE STATE TRANSACTION ONE SELECT * FROM address WHERE id=2 TRANSACTION TWO (waiting for lock....) [email protected] 1 2
  5. ..  but  most  modern  databases  today  feature  multi-­‐ version  concurrency

     control,  which  provides  a  high   degree  of  isolation  with  much  less  locking ACID Model address [email protected] 1 [email protected] 1 [email protected] 2 1 2 3 address [email protected] 1 1 address [email protected] 1 [email protected] 1 1 2 address [email protected] 1 [email protected] 1 [email protected] 2 1 2 3 INSERT INTO address (id, user_id, email) VALUES (2, 1, '[email protected]') INSERT INTO address (id, user_id, email) VALUES (3, 2, '[email protected]') DATABASE STATE TRANSACTION ONE address [email protected] 1 1 SELECT * FROM address SELECT * FROM address TRANSACTION TWO address [email protected] 1 1
  6. Configuration A Hypothetical ORM from my_orm import Entity, Integer, String,

    \ Numeric, ForeignKey, relationship class User(Entity): table = 'user' id = Integer() name = String() class Address(Entity): table = 'address' id = Integer() user_id = ForeignKey("User.id") email = String() user = relationship("User")
  7. Objects are persisted using obj.save(), deleted with object.delete() Active Record

    Persistence user1 = User(name='Ed Jones') user1.save() # emits INSERT user1.name ='Edward Jones' user1.save() # emits UPDATE address1 = Address(email='[email protected]', user=user1) address1.save() # emits INSERT address2.delete() # emits DELETE
  8. Transactions are optional, provided via implicit thread-local - else autocommit

    Active Record Persistence from my_orm import Transaction trans = Transaction.begin() user1 = User.get(id=5) user1.name = "Ed Jones" user1.save() address1 = Address(email='[email protected]', user=user1) address1.save() trans.commit()
  9. Instances not coordinated on identity - "Every object for itself!"

    Active Record Persistence >>> user1 = User.get(id=5) >>> user2 = User.get(id=5) >>> user1 is user2 False >>> user1.name = 'Ed' >>> user2.name = 'Jack' >>> user1.name 'Ed' >>> user2.name 'Jack'
  10. Active Record Persistence • The  means  of  persistence  is  provided

     via  the  interface   of  each  individual  mapped  object  -­‐  object.save(),   object.delete(),  etc. • Objects  aren't  coordinated  on  a  particular  transaction   by  default;    "autocommit",  or  transaction-­‐per-­‐ operation,  is  the  default  behavior. • The  objects  don't  otherwise  share  any  connection  to   each  other;  individual  queries  for  the  same  rows   return  different  instances. • Persist  operations  are  immediate  -­‐  an  INSERT,   UPDATE,  or  DELETE  is  emitted  directly.
  11. immediate INSERT/UPDATE means we must manually produce correct SQL order;

    can also be wasteful Active Record - Issues pending_users = [] # keep track of objects, INSERT order for record in user_records: user = User.query(name=record.username).first() if user is None: # can't save() here if we don't have all User fields user = User(name=record.username) pending_users.append(user) # can't save() here if "user" above isn't persisted yet address = Address(email_address=record.email_address) user.addresses.append(address) # ... later, need to call save() explicitly, in correct # order; may emit needless UPDATE/columns for user in pending_users: user.save() for address in user.addresses: address.save()
  12. Lack of identity coordination means we need immediate INSERT or

    UPDATE to share the state of a row Active Record - Issues def user_process_one(): user = User.get(id=5) user.name = 'Jack Jones' return user def user_process_two(): user = User.get(id=5) if user.name == 'Jack Jones': address = Address(email='[email protected]', user=user) address.save() return user user1 = user_process_one() # user_process_two() may or may not see what # user_process_one() did, depending on if we # saved() user1. user1.save() user2 = user_process_two()
  13. Ambiguous data boundaries means we don't have a clear pattern

    when to re-fetch Active Record - Issues # do this somewhere... user1 = User.get(id=5) user1.name = 'New Name' user1.save() # elsewhere, we do this... user2 = User.get(id=5) user2.name = 'Some Other Name' user2.save() # back to user1, this fails - # user1.name still says 'New Name' assert user1.name == 'Some Other Name'
  14. Uncommitted data can leak into other transactions Active Record -

    Issues queue = Queue.Queue() def user_producer(): # thread #1: produces User objects trans = Transaction.begin() for record in data: user = User.get(name=record.username) if user is None: user = User(name=record.username) user.status = record.status user.save() queue.put(user) trans.commit() def user_consumer(): # thread #2: consumes User objects while True: user = queue.get() trans = Transaction.begin() if user.status == 'D': # is this status committed or not? user.delete() # is this row persisted? trans.commit() queue.task_done()
  15. The Session Strategy • Explicit  transaction  always  present • The

     Session  maintains  a  cached  set  of  transaction   state,  consisting  of  rows. • A  row  is  typically  only  present  in  the  Session  if  it  was   selected  or  inserted  in  the  span  of  that  transaction. • Objects,  when  associated  with  a  Session,  are  proxies   for  rows,  represented  uniquely  on  primary  key   identity. • Changes  to  objects  are  pushed  out  to  rows  before   each  query,  and  at  transaction  end,  using  unit  of   work.
  16. An  object  is  said  to  be  persistent  when  it  acts

     as  a  proxy   to  a  row  present  in  the  transaction.    This  row  is  normally   always  known  as  a  result  of  a  SELECT  or  an  INSERT. The Object as Row Proxy Session Address id user_id email 1 1 [email protected] Address id user_id email 2 1 [email protected] Address id user_id email 3 2 [email protected] database transaction address (snapshot) [email protected] 1 [email protected] 1 [email protected] 2 1 2 3 address [email protected] 1 [email protected] 1 1 2 uncommitted row proxy proxy proxy
  17. With  no  transaction  present,  the  state  of  the  objects  is

      expired.      There  is  no  view  of  the  database  data  other  than   via  a  transaction. The Object as Row Proxy Session Address (expired) id user_id email <missing> <missing> <missing> Address (expired) id user_id email <missing> <missing> <missing> Address (expired) id user_id email <missing> <missing> <missing> database address [email protected] 1 [email protected] 1 1 2 [email protected] 2 3
  18. An  object  that's  outside  of  the  Session,  not  yet  

    corresponding  to  any  row,  is  said  to  be  transient. The Object as Row Proxy Session Address (transient) id user_id email <None> 2 [email protected] database address [email protected] 1 [email protected] 1 1 2
  19. An  object  that's  inside  of  the  Session,  but  not  yet

      corresponding  to  any  row,  is  said  to  be  pending. The Object as Row Proxy Session Address (pending) id user_id email 2 [email protected] database transaction address [email protected] 1 [email protected] 1 1 2
  20. A  previously  persistent  object  that's  no  longer  associated   with

     a  Session  is  said  to  be  detached.     Detachment  is  useful  for  caching,    but  not  much  else. The Object as Row Proxy Session Address (detached) id user_id email 3 2 [email protected] database address [email protected] 1 [email protected] 1 1 2 [email protected] 2 3
  21. Unit  of  work  lazily  flushes  only  those  rows/columns  that  

    have  changed,  ordering  to  maintain  consistency. Unit of Work unit of work SaveUpdateAll (User) SaveUpdateAll (Address) UPDATE user SET name='Ed Jones' WHERE id=1 UPDATE address SET email='[email protected]' WHERE id=2 INSERT INTO address (id, user_id, email) VALUES (3, 2, '[email protected]') DONE database transaction address (snapshot) [email protected] 1 [email protected] 1 [email protected] 2 1 2 3 address [email protected] 1 [email protected] 1 1 2 Session new dirty <User> <Address> <Address> user (snapshot) Ed Jones 1 user ed 1 flush() Jack 2 Jack 2 ed_user.name = 'Ed Jones' ed_user.addresses[1].email = '[email protected]' jack_user.addresses.append( Address(email='[email protected]') )
  22. Where'd the Session Come from? • Unit  of  work,  identity

     map  discussed  in  Martin  Fowler,   Patterns  of  Enterprise  Architecture • Hibernate  for  Java  largely  responsible  for  developing   Session  concepts • Java  Persistence  Architecture  (JSR-­‐220)  specifies  a   similar  model,  largely  driven  by  Hibernate • SQLAlchemy  moved  to  a  stricter,  more  correct  model   in  0.5  through  observation  of  the  Storm  ORM  for   Python
  23. unit of work handles SQL ordering and only UPDATEs what's

    changed Session for record in user_records: user = session.query(User).\ filter_by(username=record.username).first() if user is None: user = User(username=record.username) # add() to Session, we're done session.add(user) # Address is added() automatically via cascade address = Address(email_address=record.email_address) user.addresses.append(address) # emits all INSERT/UPDATE as needed in correct order, # nothing that isn't needed, INSERT ordering maintained. session.commit()
  24. identity map coordinates row state in memory Session def user_process_one(session):

    user = session.query(User).get(5) user.name = 'Jack Jones' return user def user_process_two(session): user = session.query(User).get(5) if user.name == 'Jack Jones': address = Address(email='[email protected]', user=user) return user # both functions get the same user user1 = user_process_one(session) user2 = user_process_two(session) session.commit()
  25. Explicit transactions provide clear boundaries when data should be re-fetched

    by default Session session1 = Session() user1 = session1.query(User).filter_by(id=5).one() user1.name = 'New Name' session1.commit() session2 = Session() user2 = session2.query(User).filter_by(id=5).one() user2.name = 'Some Other Name' session2.commit() # user1 was expired by the commit, reloads here assert user1.name == 'Some Other Name' # change user2 ... user2.name = 'Yet Another Name' session2.rollback() # user2 was expired by the rollback, reloads here assert user2.name == 'Some Other Name'
  26. Implicit transfer of state between transactions disallowed Session queue =

    Queue.Queue() def user_producer(): session = Session() for record in data: user = session.query(User).\ filter_by(name=record.username).first() if user is None: user = User(name=record.username) session.add(user) user.status = record.status queue.put(user) session.commit() def user_consumer(): while True: user = queue.get() session = Session() if user.status == 'D': # "user" still linked to a different session.delete(user) # transaction; raises unconditionally. session.commit() queue.task_done()
  27. User/Address Model class User(Base): __tablename__ = "user" id = Column(Integer,

    primary_key=True) name = Column(String) addresses = relationship("Address") class Address(Base): __tablename__ = "address" id = Column(Integer, primary_key=True) email = Column(String) user_id = Column(Integer, ForeignKey('user.id'))
  28. Example Code u1 = User(name="ed") u1.addresses = [ Address(email="[email protected]"), Address(email="[email protected]"),

    Address(email="[email protected]"), ] session = Session() session.add(u1) session.commit() u1.addresses[1].email = "[email protected]" session.commit()