[Checkins] SVN: z3c.zalchemy/trunk/ - Added a way to register
database specific adapters for conflict detection and
Christian Zagrodnick
cz at gocept.com
Mon Sep 24 08:00:34 EDT 2007
Log message for revision 79874:
- Added a way to register database specific adapters for conflict detection and
possible re-do by the publisher.
Changed:
U z3c.zalchemy/trunk/CHANGES.txt
U z3c.zalchemy/trunk/src/z3c/zalchemy/__init__.py
U z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py
U z3c.zalchemy/trunk/src/z3c/zalchemy/interfaces.py
U z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt
-=-
Modified: z3c.zalchemy/trunk/CHANGES.txt
===================================================================
--- z3c.zalchemy/trunk/CHANGES.txt 2007-09-24 10:14:53 UTC (rev 79873)
+++ z3c.zalchemy/trunk/CHANGES.txt 2007-09-24 12:00:34 UTC (rev 79874)
@@ -5,6 +5,9 @@
0.2 - unreleased
================
+ - Added a way to register database specific adapters for conflict detection
+ and possible re-do by the publisher.
+
- Using the threadlocal strategy of sqlalchemy instead of doing that
ourselves.
Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/__init__.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/__init__.py 2007-09-24 10:14:53 UTC (rev 79873)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/__init__.py 2007-09-24 12:00:34 UTC (rev 79874)
@@ -12,16 +12,18 @@
#
##############################################################################
-from datamanager import (
- getSession,
- inSession,
- assignTable,
- assignClass,
- createTable,
- metadata,
- getEngineForTable,
- )
+import zope.deferredimport
+zope.deferredimport.define(
+ getSession='z3c.zalchemy.datamanager:getSession',
+ inSession='z3c.zalchemy.datamanager:inSession',
+ assignTable='z3c.zalchemy.datamanager:assignTable',
+ assignClass='z3c.zalchemy.datamanager:assignClass',
+ createTable='z3c.zalchemy.datamanager:createTable',
+ metadata='z3c.zalchemy.datamanager:metadata',
+ getEngineForTable='z3c.zalchemy.datamanager:getEngineForTable')
+
+
import zope.i18nmessageid
_ = zope.i18nmessageid.MessageFactory('z3c.zalchemy')
Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py 2007-09-24 10:14:53 UTC (rev 79873)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py 2007-09-24 12:00:34 UTC (rev 79874)
@@ -21,7 +21,7 @@
from transaction.interfaces import IDataManager, ISynchronizer
from transaction.interfaces import IDataManagerSavepoint
-from interfaces import IAlchemyEngineUtility
+import z3c.zalchemy.interfaces
import sqlalchemy
from sqlalchemy.orm.mapper import global_extensions
@@ -31,7 +31,7 @@
class AlchemyEngineUtility(persistent.Persistent):
"""A utility providing a database engine.
"""
- implements(IAlchemyEngineUtility)
+ implements(z3c.zalchemy.interfaces.IAlchemyEngineUtility)
def __init__(self, name, dsn, echo=False, encoding='utf-8',
convert_unicode=False, **kwargs):
@@ -64,9 +64,9 @@
self._v_engine = None
-for name in IAlchemyEngineUtility:
+for name in z3c.zalchemy.interfaces.IAlchemyEngineUtility:
setattr(AlchemyEngineUtility, name, FieldProperty(
- IAlchemyEngineUtility[name]))
+ z3c.zalchemy.interfaces.IAlchemyEngineUtility[name]))
_tableToEngine = {}
@@ -81,7 +81,7 @@
hooked up with the Zope transaction machinery.
"""
- util = queryUtility(IAlchemyEngineUtility)
+ util = queryUtility(z3c.zalchemy.interfaces.IAlchemyEngineUtility)
if util is None:
raise ValueError("No engine utility registered")
engine = util.getEngine()
@@ -113,7 +113,8 @@
def getEngineForTable(t):
name = _tableToEngine[t]
- util = getUtility(IAlchemyEngineUtility, name=name)
+ util = getUtility(z3c.zalchemy.interfaces.IAlchemyEngineUtility,
+ name=name)
return util.getEngine()
@@ -152,7 +153,8 @@
def _assignTable(table, engine, session=None):
t = metadata.getTable(engine, table, True)
- util = getUtility(IAlchemyEngineUtility, name=engine)
+ util = getUtility(z3c.zalchemy.interfaces.IAlchemyEngineUtility,
+ name=engine)
if session is None:
session = ctx.current
session.bind_table(t, util.getEngine())
@@ -160,7 +162,8 @@
def _assignClass(class_, engine, session=None):
m = sqlalchemy.orm.class_mapper(class_)
- util = getUtility(IAlchemyEngineUtility, name=engine)
+ util = getUtility(z3c.zalchemy.interfaces.IAlchemyEngineUtility,
+ name=engine)
if session is None:
session = ctx.current
session.bind_mapper(m,util.getEngine())
@@ -174,7 +177,8 @@
def _doCreateTable(table, engine):
- util = getUtility(IAlchemyEngineUtility, name=engine)
+ util = getUtility(z3c.zalchemy.interfaces.IAlchemyEngineUtility,
+ name=engine)
t = metadata.getTable(engine, table, True)
try:
util.getEngine().create(t)
@@ -183,7 +187,8 @@
def dropTable(table, engine=''):
- util = getUtility(IAlchemyEngineUtility, name=engine)
+ util = getUtility(z3c.zalchemy.interfaces.IAlchemyEngineUtility,
+ name=engine)
t = metadata.getTable(engine, table, True)
try:
util.getEngine().drop(t)
@@ -208,7 +213,13 @@
pass
def commit(self, trans):
- self.session.flush()
+ try:
+ self.session.flush()
+ except Exception, e:
+ conflict = z3c.zalchemy.interfaces.IConflictError(e, None)
+ if conflict is None:
+ raise
+ raise conflict
def tpc_vote(self, trans):
pass
Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/interfaces.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/interfaces.py 2007-09-24 10:14:53 UTC (rev 79873)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/interfaces.py 2007-09-24 12:00:34 UTC (rev 79874)
@@ -126,3 +126,12 @@
usefull for using the engine to execute literal sql statements
"""
+
+class IConflictError(interface.Interface):
+ """Two transactions tried to modify the same object at once.
+
+ Exceptions occuring on commit/flush will be adapted to this interface.
+ Registered adapters must either return an ZODB.POSException.ConflictError
+ or None. A ConflictError will tell the publisher to retry the transaction.
+
+ """
Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt 2007-09-24 10:14:53 UTC (rev 79873)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt 2007-09-24 12:00:34 UTC (rev 79874)
@@ -195,3 +195,80 @@
>>> transaction.abort()
+
+Conflicts
+---------
+
+With the a serialisable isolation level it is possible to get conflicts with a
+relational database. For correct integration we need to convert such a conflict
+error to a ZODB.POSException.ConflictError.
+
+Since every database backend yields different exceptions an exception is
+adapted to IConflictError.
+
+Let's use a mock session here which issues a conflict error when asked to:
+
+>>> class MockSession(object):
+...
+... conflict = 'Conflict'
+...
+... def flush(self):
+... if self.conflict:
+... raise sqlalchemy.exceptions.SQLError(
+... 'UPDATE bla...', (1, 2, 3), ValueError(self.conflict))
+...
+... def create_transaction(self):
+... pass
+...
+
+Now create a datamanager:
+
+>>> dm = z3c.zalchemy.datamanager.AlchemyDataManager(MockSession())
+
+When we don't do anything we'll get the exception as specified above:
+
+>>> dm.commit(None)
+Traceback (most recent call last):
+ ...
+SQLError: (ValueError) Conflict 'UPDATE bla...' (1, 2, 3)
+
+
+When we now provide an adapter from SQLError to IConflictError we'll get a
+ZODB ConflictError:
+
+>>> import ZODB.POSException
+>>> def adapt_sqlerror(context):
+... if context.orig.args == ('Conflict', ):
+... return ZODB.POSException.ConflictError('play it again')
+>>> import zope.component
+>>> gsm = zope.component.getGlobalSiteManager()
+>>> gsm.registerAdapter(adapt_sqlerror,
+... (sqlalchemy.exceptions.SQLError, ),
+... z3c.zalchemy.interfaces.IConflictError)
+
+
+So commit:
+
+>>> dm.commit(None)
+Traceback (most recent call last):
+ ...
+ConflictError: play it again
+
+
+Note, that we added a condition to the adapter, so we'll only get a conflict
+error when the argument is "Conflict". Otherise the original exception is
+raised:
+
+>>> MockSession.conflict = 'No conflict'
+>>> dm.commit(None)
+Traceback (most recent call last):
+ ...
+SQLError: (ValueError) No conflict 'UPDATE bla...' (1, 2, 3)
+
+
+Clean up:
+
+>>> gsm.unregisterAdapter(adapt_sqlerror,
+... (sqlalchemy.exceptions.SQLError, ),
+... z3c.zalchemy.interfaces.IConflictError)
+True
More information about the Checkins
mailing list