2.4 Writing a Persistent Class

Making a Python class persistent is quite simple; it simply needs to subclass from the Persistent class, as shown in this example:

import ZODB
from Persistence import Persistent

class User(Persistent):
    pass

The apparently unnecessary import ZODB statement is needed for the following from...import statement to work correctly, since the ZODB code does some magical tricks with importing.

For simplicity, in the examples the User class will simply be used as a holder for a bunch of attributes. Normally the class would define various methods that add functionality, but that has no impact on the ZODB's treatment of the class.

The ZODB uses persistence by reachability; starting from a set of root objects, all the attributes of those objects are made persistent, whether they're simple Python data types or class instances. There's no method to explicitly store objects in a ZODB database; simply assign them as an attribute of an object, or store them in a mapping, that's already in the database. This chain of containment must eventually reach back to the root object of the database.

As an example, we'll create a simple database of users that allows retrieving a User object given the user's ID. First, we retrieve the primary root object of the ZODB using the root() method of the Connection instance. The root object behaves like a Python dictionary, so you can just add a new key/value pair for your application's root object. We'll insert a BTree object that will contain all the User objects. (The BTree module is also included as part of Zope.)

dbroot = conn.root()

# Ensure that a 'userdb' key is present 
# in the root
if not dbroot.has_key('userdb'):
    import BTree
    dbroot['userdb'] = BTree.BTree()

userdb = dbroot['userdb']

Inserting a new user is simple: create the User object, fill it with data, insert it into the BTree instance, and commit this transaction.

# Create new User instance
newuser = User() 

# Add whatever attributes you want to track
newuser.id = 'amk' 
newuser.first_name = 'Andrew' ; newuser.last_name = 'Kuchling'
...

# Add object to the BTree, keyed on the ID
userdb[newuser.id] = newuser

# Commit the change
get_transaction().commit()

When you import the ZODB package, it adds a new function, get_transaction(), to Python's collection of built-in functions. get_transaction() returns a Transaction object, which has two important methods: commit() and abort(). commit() writes any modified objects to disk, making the changes permanent, while abort() rolls back any changes that have been made, restoring the original state of the objects. If you're familiar with database transactional semantics, this is all what you'd expect.

Because the integration with Python is so complete, it's a lot like having transactional semantics for your program's variables, and you can experiment with transactions at the Python interpreter's prompt:

>>> newuser
<User instance at 81b1f40>
>>> newuser.first_name           # Print initial value
'Andrew'         
>>> newuser.first_name = 'Bob'   # Change first name
>>> newuser.first_name           # Verify the change
'Bob'
>>> get_transaction().abort()    # Abort transaction
>>> newuser.first_name           # The value has changed back
'Andrew'