[Checkins] SVN: z3c.zalchemy/trunk/ Merging changes from
`ctheune-automatic-session` branch.
Christian Theune
ct at gocept.com
Thu Jun 28 02:03:10 EDT 2007
Log message for revision 77165:
Merging changes from `ctheune-automatic-session` branch.
Changed:
U z3c.zalchemy/trunk/CHANGES.txt
U z3c.zalchemy/trunk/setup.py
U z3c.zalchemy/trunk/src/z3c/zalchemy/README.txt
U z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py
U z3c.zalchemy/trunk/src/z3c/zalchemy/metaconfigure.py
U z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt
U z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_directives.py
U z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_zalchemy.py
-=-
Modified: z3c.zalchemy/trunk/CHANGES.txt
===================================================================
--- z3c.zalchemy/trunk/CHANGES.txt 2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/CHANGES.txt 2007-06-28 06:03:09 UTC (rev 77165)
@@ -5,7 +5,16 @@
0.2 - unreleased
================
+ - Provide a tighter integration with Zope's transaction mechanism. Sessions
+ are now automatically associated with new objects. We rely on SQLAlchemy's
+ SessionContext object which hands out a session for each thread. Your code
+ rarely should never have to call `session.save(object)` now.
+ One incompatible change was introduced: You can not call `getSession`
+ before registering an (unnamed) engine utility first. Doing so will raise
+ a ValueError.
+
+
0.1.1 - 2007-06-27
==================
Modified: z3c.zalchemy/trunk/setup.py
===================================================================
--- z3c.zalchemy/trunk/setup.py 2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/setup.py 2007-06-28 06:03:09 UTC (rev 77165)
@@ -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/trunk/src/z3c/zalchemy/README.txt
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/README.txt 2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/README.txt 2007-06-28 06:03:09 UTC (rev 77165)
@@ -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/trunk/src/z3c/zalchemy/datamanager.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py 2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py 2007-06-28 06:03:09 UTC (rev 77165)
@@ -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,50 +77,77 @@
_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():
+ """Creates a new session that is bound to the default engine utility and
+ hooked up with the Zope transaction machinery.
+
+ """
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)
+
+ # This session is now only bound to the default engine. We need to bind
+ # the other explicitly bound tables and classes as well.
+ bind_session(session)
+
+ transaction.get().join(AlchemyDataManager(session))
return session
+def bind_session(session):
+ """Applies all table and class bindings to the given session."""
+ for table, engine in _tableToEngine.items():
+ _assignTable(table, engine, session)
+ for class_, engine in _classToEngine.items():
+ _assignClass(class_, engine, 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):
+def assignTable(table, engine, immediate=True):
+ """Assign a table to an engine and propagate the binding to the current
+ session.
+
+ The binding is not applied to the current session if `immediate` is False.
+
+ """
_tableToEngine[table]=engine
- _assignTable(table, engine)
+ if immediate:
+ _assignTable(table, engine)
-def assignClass(class_, engine):
+def assignClass(class_, engine, immediate=True):
+ """Assign a class to an engine and propagate the binding to the current
+ session.
+
+ The binding is not applied to the current session if `immediate` is False.
+
+ """
_classToEngine[class_]=engine
- _assignClass(class_, engine)
+ if immediate:
+ _assignClass(class_, engine)
def createTable(table, engine):
@@ -126,26 +155,27 @@
_createTables()
-def _assignTable(table, engine):
- if inSession():
- t = metadata.getTable(engine, table, True)
- util = getUtility(IAlchemyEngineUtility, name=engine)
- _storage.session.bind_table(t,util.getEngine())
+def _assignTable(table, engine, session=None):
+ t = metadata.getTable(engine, table, True)
+ util = getUtility(IAlchemyEngineUtility, name=engine)
+ if session is None:
+ session = ctx.current
+ session.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())
+def _assignClass(class_, engine, session=None):
+ m = sqlalchemy.orm.class_mapper(class_)
+ util = getUtility(IAlchemyEngineUtility, name=engine)
+ if session is None:
+ session = ctx.current
+ session.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 +196,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. """
-
-class AlchemyDataManager(object):
- """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 +220,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/trunk/src/z3c/zalchemy/metaconfigure.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/metaconfigure.py 2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/metaconfigure.py 2007-06-28 06:03:09 UTC (rev 77165)
@@ -19,6 +19,7 @@
import z3c.zalchemy
+
def engine(_context, url, name='', echo=False, **kwargs):
engine = AlchemyEngineUtility(name, url, echo=echo, **kwargs)
utility(_context,
@@ -27,12 +28,26 @@
permission=PublicPermission,
name=name)
+
def connectTable(_context, table, engine):
- z3c.zalchemy.assignTable(table, engine)
+ _context.action(
+ discriminator=('zalchemy.table', table),
+ callable=z3c.zalchemy.assignTable,
+ args=(table, engine, False)
+ )
+
def connectClass(_context, class_, engine):
- z3c.zalchemy.assignClass(class_, engine)
+ _context.action(
+ discriminator=('zalchemy.class', class_),
+ callable=z3c.zalchemy.assignClass,
+ args=(class_, engine, False)
+ )
+
def createTable(_context, table, engine):
- z3c.zalchemy.createTable(table, engine)
-
+ _context.action(
+ discriminator=('zalchemy.create-table', table),
+ callable=z3c.zalchemy.createTable,
+ args=(table, engine)
+ )
Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt 2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt 2007-06-28 06:03:09 UTC (rev 77165)
@@ -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
Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_directives.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_directives.py 2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_directives.py 2007-06-28 06:03:09 UTC (rev 77165)
@@ -85,7 +85,7 @@
/>
'''
)))
- util = component.getUtility(IAlchemyEngineUtility,'sqlite-in-memory')
+ util = component.getUtility(IAlchemyEngineUtility, 'sqlite-in-memory')
self.assert_(len(z3c.zalchemy.datamanager._tableToEngine)==1)
self.assert_('testTable' in z3c.zalchemy.datamanager._tableToEngine)
self.assert_(mappedTestClass in z3c.zalchemy.datamanager._classToEngine)
Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_zalchemy.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_zalchemy.py 2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_zalchemy.py 2007-06-28 06:03:09 UTC (rev 77165)
@@ -56,9 +56,9 @@
z3c.zalchemy.testing.tearDown(self)
def testNoDefaultEngine(self):
- session = z3c.zalchemy.getSession()
- self.assertNotEqual(session, None)
- self.assertEqual(session.get_bind(None), None)
+ # Our session can't work without an engine. If we did not
+ # register an IAlchemyEngineUtility, we can't access the session
+ self.assertRaises(ValueError, z3c.zalchemy.getSession)
def testDefaultEngine(self):
from zope.component import provideUtility
More information about the Checkins
mailing list