Zope transactions and sqlalchemy
================================

When a zope transaction is used also a sqlalchemy transaction must be
activated. "z3c.zalchemy" installs a data manager every time a new zope
transaction is created. 

Create a utility to provide a database :

  >>> import os
  >>> from zope.component import provideUtility
  >>> from z3c.zalchemy.interfaces import IAlchemyEngineUtility
  >>> from z3c.zalchemy.datamanager import AlchemyEngineUtility
  >>> engineUtility = AlchemyEngineUtility('database',
  ...                                      'sqlite:///%s'%dbTrFilename,
  ...                                      echo=False)
  >>> provideUtility(engineUtility, IAlchemyEngineUtility)

Setup a sqlalchemy table and class :

  >>> import sqlalchemy
  >>> import z3c.zalchemy
  >>> table2 = sqlalchemy.Table(
  ...     'table2',
  ...     z3c.zalchemy.metadata(),
  ...     sqlalchemy.Column('id', sqlalchemy.Integer,
  ...         sqlalchemy.Sequence('atable_id'), primary_key = True),
  ...     sqlalchemy.Column('value', sqlalchemy.Integer),
  ...     )
  >>> class A(object):
  ...   pass
  >>> A.mapper = sqlalchemy.mapper(A, table2)

Now start a zope transaction :

  >>> import transaction
  >>> txn = transaction.begin()

Get a thread local session :

  >>> session = z3c.zalchemy.getSession()

  >>> z3c.zalchemy.createTable('table2', '')

Multiple calls to getSession return the same session :

  >>> session == z3c.zalchemy.getSession()
  True

Create an object and add it to the session :

  >>> a=A()
  >>> session.save(a)
  >>> a.id is None
  True

We flush the object so that it gets a primary key.

  >>> session.flush([a])
  >>> a.id is None
  False

  >>> a.value = 1

  >>> transaction.commit()

After the commit we can get a new session from zalchemy outside of a
transaction.
We can tell zalchemy to create a new transaction if there is none active.
But we need to commit the transaction manually.

  >>> session2 = z3c.zalchemy.getSession(True)
  >>> session == session2
  False
  >>> a=A()
  >>> session2.save(a)
  >>> a.value = 2
  >>> transaction.commit()


Handling multiple threads
-------------------------

  >>> import threading

A different thread must get a different session :

  >>> log = []
  >>> def differentSession():
  ...     global session
  ...     log.append(('differentSession',session == z3c.zalchemy.getSession()))
  ...

  >>> thread = threading.Thread(target=differentSession)
  >>> thread.start()
  >>> thread.join()
  >>> log
  [('differentSession', False)]

A different Thread must be able to operate on the engine :

  >>> log = []
  >>> def modifyA():
  ...     txn = transaction.begin()
  ...     session = z3c.zalchemy.getSession()
  ...     obj = session.get(A, 1)
  ...     obj.value+= 1
  ...     log.append(('modifyA', obj.value))
  ...     transaction.commit()
  ...

  >>> thread = threading.Thread(target=modifyA)
  >>> thread.start()
  >>> thread.join()
  >>> log
  [('modifyA', 2)]

Nested Threads :

  >>> log = []

  >>> def nested():
  ...     txn = transaction.begin()
  ...     session = z3c.zalchemy.getSession()
  ...     obj = session.get(A, 1)
  ...     thread = threading.Thread(target=modifyA)
  ...     thread.start()
  ...     thread.join()
  ...     obj.value+= 1
  ...     log.append(('nested', obj.value))
  ...     transaction.commit()
  ...

  >>> thread = threading.Thread(target=nested)
  >>> thread.start()
  >>> thread.join()
  >>> log
  [('modifyA', 3), ('nested', 3)]


Aborting transactions
---------------------

  >>> session = z3c.zalchemy.getSession(True)
  >>> a=session.get(A, 1)
  >>> v = a.value
  >>> a.value += 1
  >>> session.flush([a])
  >>> transaction.abort()

  >>> session = z3c.zalchemy.getSession(True)
  >>> a=session.get(A, 1)
  >>> a.value
  3
  
  >>> a.value == v
  True


Two Phase Commit With Errors
----------------------------

zalchemy uses zope's two phase commit by first doing only a flush when commit
is called. SQLAlchemy's transaction is commited in the second phase of the
zope transacion.

  >>> session = z3c.zalchemy.getSession(True)
  >>> aa=A()
  >>> session.save(aa)
  >>> aa.value = 3

We create an object with an already existing primary key.

  >>> aa.id = 2

Let's make sure we get an exception when using commit.

  >>> from z3c.zalchemy.datamanager import _storage
  >>> _storage.dataManager.commit(transaction.manager.get())
  Traceback (most recent call last):
  ...
  SQLError: (IntegrityError) PRIMARY KEY must be unique u'INSERT INTO table2 (id, value) VALUES (?, ?)' [2, 3]

Finally we need to do an abort zope's transaction.

  >>> transaction.abort()

And we do the same using the commit from the transaction.

  >>> session = z3c.zalchemy.getSession(True)
  >>> aa=A()
  >>> session.save(aa)
  >>> aa.value = 3
  >>> aa.id = 2
  >>> transaction.commit()
  Traceback (most recent call last):
  ...
  SQLError: (IntegrityError) PRIMARY KEY must be unique u'INSERT INTO table2 (id, value) VALUES (?, ?)' [2, 3]

We need to manually abort the transaction.

  >>> transaction.abort()

