Transactions and Versioning
Committing and Aborting
Changes made during a transaction don’t appear in the database until the
transaction commits. This is done by calling the commit()
method of the
current Transaction
object, where the latter is obtained from the
get()
method of the current transaction manager. If the default thread
transaction manager is being used, then transaction.commit()
suffices.
Similarly, a transaction can be explicitly aborted (all changes within the
transaction thrown away) by invoking the abort()
method of the current
Transaction
object, or simply transaction.abort()
if using the
default thread transaction manager.
Prior to ZODB 3.3, if a commit failed (meaning the commit()
call raised an
exception), the transaction was implicitly aborted and a new transaction was
implicitly started. This could be very surprising if the exception was
suppressed, and especially if the failing commit was one in a sequence of
subtransaction commits.
So, starting with ZODB 3.3, if a commit fails, all further attempts to commit,
join, or register with the transaction raise
ZODB.POSException.TransactionFailedError
. You must explicitly start a
new transaction then, either by calling the abort()
method of the current
transaction, or by calling the begin()
method of the current transaction’s
transaction manager.
Subtransactions
Subtransactions can be created within a transaction. Each subtransaction can be individually committed and aborted, but the changes within a subtransaction are not truly committed until the containing transaction is committed.
The primary purpose of subtransactions is to decrease the memory usage of transactions that touch a very large number of objects. Consider a transaction during which 200,000 objects are modified. All the objects that are modified in a single transaction have to remain in memory until the transaction is committed, because the ZODB can’t discard them from the object cache. This can potentially make the memory usage quite large. With subtransactions, a commit can be be performed at intervals, say, every 10,000 objects. Those 10,000 objects are then written to permanent storage and can be purged from the cache to free more space.
To commit a subtransaction instead of a full transaction, pass a true value to
the commit()
or abort()
method of the Transaction
object.
# Commit a subtransaction
transaction.commit(True)
# Abort a subtransaction
transaction.abort(True)
A new subtransaction is automatically started upon successful committing or aborting the previous subtransaction.
Undoing Changes
Some types of Storage
support undoing a transaction even after it’s
been committed. You can tell if this is the case by calling the
supportsUndo()
method of the DB
instance, which returns true if
the underlying storage supports undo. Alternatively you can call the
supportsUndo()
method on the underlying storage instance.
If a database supports undo, then the undoLog(start, end[, func])()
method
on the DB
instance returns the log of past transactions, returning
transactions between the times start and end, measured in seconds from the
epoch. If present, func is a function that acts as a filter on the
transactions to be returned; it’s passed a dictionary representing each
transaction, and only transactions for which func returns true will be
included in the list of transactions returned to the caller of undoLog()
.
The dictionary contains keys for various properties of the transaction. The
most important keys are id
, for the transaction ID, and time
, for the
time at which the transaction was committed.
>>> print storage.undoLog(0, sys.maxint)
[{'description': '',
'id': 'AzpGEGqU/0QAAAAAAAAGMA',
'time': 981126744.98,
'user_name': ''},
{'description': '',
'id': 'AzpGC/hUOKoAAAAAAAAFDQ',
'time': 981126478.202,
'user_name': ''}
...
To store a description and a user name on a commit, get the current transaction
and call the note(text)()
method to store a description, and the
setUser(user_name)()
method to store the user name. While setUser()
overwrites the current user name and replaces it with the new value, the
note()
method always adds the text to the transaction’s description, so it
can be called several times to log several different changes made in the course
of a single transaction.
transaction.get().setUser('amk')
transaction.get().note('Change ownership')
To undo a transaction, call the DB.undo(id)()
method, passing it the ID of
the transaction to undo. If the transaction can’t be undone, a
ZODB.POSException.UndoError
exception will be raised, with the message
“non-undoable transaction”. Usually this will happen because later transactions
modified the objects affected by the transaction you’re trying to undo.
After you call undo()
you must commit the transaction for the undo to
actually be applied. [1] There is one glitch in the undo process. The thread
that calls undo may not see the changes to the object until it calls
Connection.sync()
or commits another transaction.
Versions
Warning
Versions should be avoided. They’re going to be deprecated, replaced by better approaches to long-running transactions.
While many subtransactions can be contained within a single regular transaction, it’s also possible to contain many regular transactions within a long-running transaction, called a version in ZODB terminology. Inside a version, any number of transactions can be created and committed or rolled back, but the changes within a version are not made visible to other connections to the same ZODB.
Not all storages support versions, but you can test for versioning ability by
calling supportsVersions()
method of the DB
instance, which
returns true if the underlying storage supports versioning.
A version can be selected when creating the Connection
instance using
the DB.open([*version*])()
method. The version argument must be a string
that will be used as the name of the version.
vers_conn = db.open(version='Working version')
Transactions can then be committed and aborted using this versioned connection.
Other connections that don’t specify a version, or provide a different version
name, will not see changes committed within the version named Working
version
. To commit or abort a version, which will either make the changes
visible to all clients or roll them back, call the DB.commitVersion()
or
DB.abortVersion()
methods. XXX what are the source and dest arguments for?
The ZODB makes no attempt to reconcile changes between different versions.
Instead, the first version which modifies an object will gain a lock on that
object. Attempting to modify the object from a different version or from an
unversioned connection will cause a ZODB.POSException.VersionLockError
to
be raised:
from ZODB.POSException import VersionLockError
try:
transaction.commit()
except VersionLockError, (obj_id, version):
print ('Cannot commit; object %s '
'locked by version %s' % (obj_id, version))
The exception provides the ID of the locked object, and the name of the version having a lock on it.
Multithreaded ZODB Programs
ZODB databases can be accessed from multithreaded Python programs. The
Storage
and DB
instances can be shared among several threads,
as long as individual Connection
instances are created for each thread.
Footnotes