[Checkins] SVN: z3c.zalchemy/branches/ctheune-automatic-session/s Snapshot on my work for automatic session assignment. Three tests are

Christian Theune ct at gocept.com
Wed Jun 27 12:56:54 EDT 2007


Log message for revision 77144:
  Snapshot on my work for automatic session assignment. Three tests are
  currently failing for me, but the general scheme works.
  

Changed:
  U   z3c.zalchemy/branches/ctheune-automatic-session/setup.py
  U   z3c.zalchemy/branches/ctheune-automatic-session/src/z3c/zalchemy/README.txt
  U   z3c.zalchemy/branches/ctheune-automatic-session/src/z3c/zalchemy/datamanager.py
  U   z3c.zalchemy/branches/ctheune-automatic-session/src/z3c/zalchemy/tests/TRANSACTION.txt

-=-
Modified: z3c.zalchemy/branches/ctheune-automatic-session/setup.py
===================================================================
--- z3c.zalchemy/branches/ctheune-automatic-session/setup.py	2007-06-27 16:55:57 UTC (rev 77143)
+++ z3c.zalchemy/branches/ctheune-automatic-session/setup.py	2007-06-27 16:56:53 UTC (rev 77144)
@@ -20,7 +20,7 @@
                         'zope.schema',
                         'zope.app.testing',
                         'zope.app.component',
-                        'zope.app.keyreference<3.5dev',
+                        'zope.app.keyreference',
                         'zope.app.container',
                         'zope.app.pagetemplate',
                        ],

Modified: z3c.zalchemy/branches/ctheune-automatic-session/src/z3c/zalchemy/README.txt
===================================================================
--- z3c.zalchemy/branches/ctheune-automatic-session/src/z3c/zalchemy/README.txt	2007-06-27 16:55:57 UTC (rev 77143)
+++ z3c.zalchemy/branches/ctheune-automatic-session/src/z3c/zalchemy/README.txt	2007-06-27 16:56:53 UTC (rev 77144)
@@ -92,50 +92,29 @@
   >>> provideUtility(engineUtility, IAlchemyEngineUtility)
 
 Tables can be created without an open transaction or session.
-If no session is created then the table creation is deffered to the next
+If no session is created then the table creation is deferred to the next
 call to zalchemy.getSession.
 
   >>> z3c.zalchemy.createTable('table3', '')
 
-Note that the transaction handling is done inside Zope.
+zalchemy automatically coordinates Zope's transaction manager with
+SQLAlchemy's sessions. All mapped classes are automatically associated with
+thread-local session, which in turn is automatically connected to a special
+data manager that coordinates with Zope's transactions.
 
-  >>> import transaction
-  >>> txn = transaction.begin()
-
-Everything inside SQLAlchemy needs a Session. We must obtain the Session
-from zalchemy. This makes sure that a transaction handler is inserted into
-Zope's transaction process.
-
-To simplify the usage of getSession we store the function in "session" (see
-also the note above).
-
-  >>> session = z3c.zalchemy.getSession
-
   >>> a = A()
   >>> a.value = 1
 
-Apply the new object to the session :
+Committing a transaction will automatically trigger a flush and clear the
+session.
 
-  >>> session().save(a)
-
-A new instance of a mapped sqlobject class is created. This object is not
-stored in the database until the session is committed or flush is called for
-the new instance.
-
-To be able to query a new instance it is therefore necessary to flush the
-object to the database before the query.
-
-  >>> session().flush([a])
-
-Commiting a transaction is doing the same with all remaining instances.
-After this commit the current session is flushed and cleared.
-
+  >>> import transaction
   >>> transaction.commit()
 
-Now let's try to get the object back in a new transaction :
+Now let's try to get the object back in a new transaction (we're in a new
+transaction already because the old transaction was committed):
 
-  >>> txn = transaction.begin()
-
+  >>> from z3c.zalchemy.datamanager import getSession as session
   >>> a = session().get(A, 1)
   >>> a.value
   1
@@ -146,9 +125,9 @@
 Multiple databases
 ------------------
 
-The above example asumed that there is only one database.
-The database engine was registered as unnamed utility.
-The unnamed utility is always the default database for new sessions.
+The above example assumed that there is only one database.  The database
+engine was registered as an unnamed utility.  The unnamed utility is always
+the default database for new sessions.
 
 This automatically assigns every table to the default engine.
 
@@ -179,8 +158,6 @@
   ...     pass
   >>> B.mapper = sqlalchemy.mapper(B, bTable)
 
-  >>> txn = transaction.begin()
-
 Assign bTable to the new engine and create the table.
 This time we do it inside of a session.
 
@@ -188,17 +165,13 @@
   >>> z3c.zalchemy.createTable('bTable', 'engine2')
 
   >>> b = B()
-  >>> session().save(b)
   >>> b.value = 'b1'
 
   >>> a = A()
-  >>> session().save(a)
   >>> a.value = 321
 
   >>> transaction.commit()
 
-  >>> txn = transaction.begin()
-
   >>> a = session().get(A, 1)
   >>> b = session().get(B, 1)
   >>> str(b.value)
@@ -222,10 +195,7 @@
 
   >>> z3c.zalchemy.createTable('table3', 'engine2')
 
-  >>> txn = transaction.begin()
-
   >>> aa = Aa()
-  >>> session().save(aa)
   >>> aa.value = 100
 
   >>> transaction.commit()

Modified: z3c.zalchemy/branches/ctheune-automatic-session/src/z3c/zalchemy/datamanager.py
===================================================================
--- z3c.zalchemy/branches/ctheune-automatic-session/src/z3c/zalchemy/datamanager.py	2007-06-27 16:55:57 UTC (rev 77143)
+++ z3c.zalchemy/branches/ctheune-automatic-session/src/z3c/zalchemy/datamanager.py	2007-06-27 16:56:53 UTC (rev 77144)
@@ -25,8 +25,10 @@
 from interfaces import IAlchemyEngineUtility
 
 import sqlalchemy
+from sqlalchemy.orm.mapper import global_extensions
+from sqlalchemy.ext.sessioncontext import SessionContext
+from sqlalchemy.orm.session import Session
 
-
 class AlchemyEngineUtility(persistent.Persistent):
     """A utility providing a database engine.
     """
@@ -75,40 +77,35 @@
 _tableToEngine = {}
 _classToEngine = {}
 _tablesToCreate = []
-_storage = local()
 
-def getSession(createTransaction=False):
-    session=getattr(_storage,'session',None)
-    if session:
-        return session
-    txn = transaction.manager.get()
-    if createTransaction and (txn is None):
-        txn = transaction.begin()
+# SQLAlchemy session management through thread-locals and our own data
+# manager.
+
+def createSession():
     util = queryUtility(IAlchemyEngineUtility)
-    engine = None
-    if util is not None:
-        engine = util.getEngine()
-    _storage.session=sqlalchemy.create_session(bind_to=engine)
-    session = _storage.session
-    for table, engine in _tableToEngine.iteritems():
-        _assignTable(table, engine)
-    for class_, engine in _classToEngine.iteritems():
-        _assignClass(class_, engine)
-    if txn is not None:
-        _storage.dataManager = AlchemyDataManager(session)
-        txn.join(_storage.dataManager)
-    _createTables()
+    if util is None:
+        raise ValueError("No engine utility registered")
+    engine = util.getEngine()
+    session = sqlalchemy.create_session(bind_to=engine)
+    transaction.get().join(AlchemyDataManager(session))
     return session
 
+ctx = SessionContext(createSession)
+global_extensions.append(ctx.mapper_extension)
 
+
+def getSession():
+    return ctx.current
+
+
 def getEngineForTable(t):
     name = _tableToEngine[t]
     util = getUtility(IAlchemyEngineUtility, name=name)
     return util.getEngine()
-    
 
+
 def inSession():
-    return getattr(_storage,'session',None) is not None
+    return True
 
 
 def assignTable(table, engine):
@@ -127,25 +124,22 @@
 
 
 def _assignTable(table, engine):
-    if inSession():
-        t = metadata.getTable(engine, table, True)
-        util = getUtility(IAlchemyEngineUtility, name=engine)
-        _storage.session.bind_table(t,util.getEngine())
+    t = metadata.getTable(engine, table, True)
+    util = getUtility(IAlchemyEngineUtility, name=engine)
+    ctx.current.bind_table(t, util.getEngine())
 
 
 def _assignClass(class_, engine):
-    if inSession():
-        m = sqlalchemy.orm.class_mapper(class_)
-        util = getUtility(IAlchemyEngineUtility, name=engine)
-        _storage.session.bind_mapper(m,util.getEngine())
+    m = sqlalchemy.orm.class_mapper(class_)
+    util = getUtility(IAlchemyEngineUtility, name=engine)
+    ctx.current.bind_mapper(m,util.getEngine())
 
 
 def _createTables():
-    if inSession():
-        tables = _tablesToCreate[:]
-        del _tablesToCreate[:]
-        for table, engine in tables:
-            _doCreateTable(table, engine)
+    tables = _tablesToCreate[:]
+    del _tablesToCreate[:]
+    for table, engine in tables:
+        _doCreateTable(table, engine)
 
 
 def _doCreateTable(table, engine):
@@ -166,28 +160,18 @@
         pass
 
 
-def _dataManagerFinished():
-    _storage.session = None
-    _storage.dataManager = None
-    utils = getUtilitiesFor(IAlchemyEngineUtility)
-    for util in utils:
-        util[1]._resetEngine()
-
-
 class AlchemyDataManager(object):
-    """Takes care of the transaction process in zope.
-    """
+    """Takes care of the transaction process in Zope. """
+
     implements(IDataManager)
 
-    _commitFailed = False
-
     def __init__(self, session):
         self.session = session
         self.transaction = session.create_transaction()
 
     def abort(self, trans):
         self.transaction.rollback()
-        _dataManagerFinished()
+        self._cleanup()
 
     def tpc_begin(self, trans):
         pass
@@ -200,16 +184,23 @@
 
     def tpc_finish(self, trans):
         self.transaction.commit()
-        _dataManagerFinished()
+        self._cleanup()
 
     def tpc_abort(self, trans):
         self.transaction.rollback()
-        _dataManagerFinished()
+        self._cleanup()
 
     def sortKey(self):
         return str(id(self))
 
+    def _cleanup(self):
+        self.session.clear()
+        del ctx.current
+        utils = getUtilitiesFor(IAlchemyEngineUtility)
+        for name, util in utils:
+            util._resetEngine()
 
+
 class MetaManager(object):
     """A manager for metadata to be able to use the same table name in
     different databases.

Modified: z3c.zalchemy/branches/ctheune-automatic-session/src/z3c/zalchemy/tests/TRANSACTION.txt
===================================================================
--- z3c.zalchemy/branches/ctheune-automatic-session/src/z3c/zalchemy/tests/TRANSACTION.txt	2007-06-27 16:55:57 UTC (rev 77143)
+++ z3c.zalchemy/branches/ctheune-automatic-session/src/z3c/zalchemy/tests/TRANSACTION.txt	2007-06-27 16:56:53 UTC (rev 77144)
@@ -65,14 +65,11 @@
   >>> 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.
+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 = z3c.zalchemy.getSession()
+  >>> a = A()
   >>> session2.save(a)
   >>> a.value = 2
   >>> transaction.commit()
@@ -88,7 +85,7 @@
   >>> log = []
   >>> def differentSession():
   ...     global session
-  ...     log.append(('differentSession',session == z3c.zalchemy.getSession()))
+  ...     log.append(('differentSession', session == z3c.zalchemy.getSession()))
   ...
 
   >>> thread = threading.Thread(target=differentSession)
@@ -104,10 +101,9 @@
   ...     txn = transaction.begin()
   ...     session = z3c.zalchemy.getSession()
   ...     obj = session.get(A, 1)
-  ...     obj.value+= 1
+  ...     obj.value += 1
   ...     log.append(('modifyA', obj.value))
   ...     transaction.commit()
-  ...
 
   >>> thread = threading.Thread(target=modifyA)
   >>> thread.start()
@@ -115,7 +111,7 @@
   >>> log
   [('modifyA', 2)]
 
-Nested Threads :
+Nested Threads:
 
   >>> log = []
 
@@ -129,7 +125,6 @@
   ...     obj.value+= 1
   ...     log.append(('nested', obj.value))
   ...     transaction.commit()
-  ...
 
   >>> thread = threading.Thread(target=nested)
   >>> thread.start()
@@ -141,20 +136,20 @@
 Aborting transactions
 ---------------------
 
-  >>> session = z3c.zalchemy.getSession(True)
-  >>> a=session.get(A, 1)
-  >>> v = a.value
+  >>> session = z3c.zalchemy.getSession()
+  >>> a = session.get(A, 1)
+  >>> a.value = 2
+  >>> transaction.commit()
+
   >>> a.value += 1
-  >>> session.flush([a])
+  >>> a.value
+  3
   >>> transaction.abort()
 
-  >>> session = z3c.zalchemy.getSession(True)
-  >>> a=session.get(A, 1)
+  >>> session = z3c.zalchemy.getSession()
+  >>> a = session.get(A, 1)
   >>> a.value
-  3
-  
-  >>> a.value == v
-  True
+  2
 
 
 Two Phase Commit With Errors
@@ -164,7 +159,7 @@
 is called. SQLAlchemy's transaction is commited in the second phase of the
 zope transacion.
 
-  >>> session = z3c.zalchemy.getSession(True)
+  >>> session = z3c.zalchemy.getSession()
   >>> aa=A()
   >>> session.save(aa)
   >>> aa.value = 3
@@ -175,19 +170,18 @@
 
 Let's make sure we get an exception when using commit.
 
-  >>> from z3c.zalchemy.datamanager import _storage
-  >>> _storage.dataManager.commit(transaction.manager.get())
+  >>> transaction.commit()
   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.
+Finally we need to abort zope's transaction.
 
   >>> transaction.abort()
 
 And we do the same using the commit from the transaction.
 
-  >>> session = z3c.zalchemy.getSession(True)
+  >>> session = z3c.zalchemy.getSession()
   >>> aa=A()
   >>> session.save(aa)
   >>> aa.value = 3



More information about the Checkins mailing list