19.6. The ZODB Object-Oriented Database
ZODB, the Zope Object Database, is a
ZODB is an
Although ZODB does not support SQL queries, objects stored in ZODB can leverage the full power of the Python language. Moreover, in some applications, stored data is more naturally represented as a structured Python object. Table-based relational systems often must represent such data as
Using a ZODB database is very similar to Python's standard library shelves, described in the prior section. Just like shelves, ZODB uses the Python
In fact, there is almost no database interface to be foundobjects are made persistent simply by assigning them to keys of the root ZODB dictionary object, or embedding them in objects stored in the database root. And as in a shelve, records take the form of native Python objects,
Unlike shelves, ZODB adds features critical to some types of programs:
Because of such advantages, ZODB is probably worth your attention if you need to store Python objects in a database persistently, in a production environment. The only significant price you'll pay for using ZODB is a small amount of extra code:
Considering the extra functionality ZODB provides beyond shelves, these trade-offs are usually more than justified for many applications. 19.6.1. A ZODB Tutorial
To sample the flavor of ZODB, let's work through a quick interactive tutorial. We'll
19.6.1.1. Installing ZODB
The first thing we need to do is install ZODB on top of Python. ZODB is an open source package, but it is not a standard part of Python today; it must be
ZODB is available in both source and self-installer forms. On Windows, ZODB is available as a self-installing executable, which
Moreover, much like Python's standard
19.6.1.2. The ZEO distributed object server
More generally, ZEO, for Zope Enterprise Objects, adds a distributed object architecture to applications requiring high performance and scalability. To understand how, you have to understand the architecture of ZODB itself. ZODB works by routing object
ZEO itself consists of a TCP/IP socket server and the new storage interface object used by clients. The ZEO server may run on the same or a remote machine. Upon receipt, the server
In the interest of space, we'll finesse further ZODB and ZEO details here; see other resources for more details on ZEO and ZODB's concurrent updates model. To most programs, ZODB is surprisingly easy to use; let's turn to some real code
19.6.1.3. Creating a ZODB databaseOnce you've installed ZODB, its interface takes the form of packages and modules to your code. Let's create a first database to see how this works: ...\PP3E\Database\ZODBscripts\> python >>> from ZODB import FileStorage, DB >>> storage = FileStorage.FileStorage(r'C:\Mark\temp\mydb.fs') >>> db = DB(storage) >>> connection = db.open( ) >>> root = connection.root( ) This is mostly standard code for connecting to a ZODB database: we import its tools, create a FileStorage and a DB from it, and then open the database and create the root object . The root object is the persistent dictionary in which objects are stored. FileStorage is an object that maps the database to a flat file. Other storage interface options, such as relational database-based storage, are also possible. When using the ZEO server configuration discussed earlier, programs import a ClientStorage interface object from the ZEO package instead, but the rest of the code is the same. Now that we have a database, let's add a few objects to it. Almost any Python object will do, including tuples, lists, dictionaries, class instances, and nested combinations thereof. Simply assign your objects to a key in the database root object to make them persistent:
>>>
object1 = (1, 'spam', 4, 'YOU')
>>>
object2 = [[1, 2, 3], [4, 5, 6]]
>>>
object2.append([7, 8, 9])
>>>
object2
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>>
>>>
object3 = {'name': ['Bob', 'Doe'],
'age': 42,
'job': ('dev', 'mgr')}
>>>
>>>
root['mystr'] = 'spam' * 3
>>>
root['mytuple'] = object1
>>>
root['mylist'] = object2
>>>
root['mydict'] = object3
>>>
root['mylist']
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Because ZODB supports transaction
>>> import transaction >>> transaction.commit( ) >>> storage.close( ) ...\PP3E\Database\ZODBscripts> dir /B c:\mark\temp\mydb* mydb.fs mydb.fs.index mydb.fs.tmp Without the final commit in this session, none of the changes we made would be saved. This is what we want in generalif a program aborts in the middle of an update task, none of the partially complete work it has done is retained. 19.6.1.4. Fetching and changing
OK; we've made a few objects persistent in our ZODB. Pulling them back in another session or program is just as straightforward: reopen the database as before and index the root to fetch objects back into memory. The database root supports dictionary interfacesit may be indexed, has dictionary
...\PP3E\Database\ZODBscripts\>
python
>>>
from ZODB import FileStorage, DB
>>>
storage = FileStorage.FileStorage(r'C:\Mark\temp\mydb.fs')
>>>
db = DB(storage)
>>>
connection = db.open( )
>>>
root = connection.root( ) # connect
>>>
>>>
print len(root), root.keys( ) # size, index
4 ['mylist', 'mystr', 'mytuple', 'mydict']
>>>
>>>
print root['mylist'] # fetch objects
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>>
print root['mydict']
{'job': ('dev', 'mgr'), 'age': 42, 'name': ['Bob', 'Doe']}
>>>
root['mydict']['name'][-1] # Bob's last name
'Doe'
Because the database root looks just like a dictionary, we can process it with normal dictionary codestepping through the keys list to scan record by record, for instance:
>>>
for key in root.keys( ):
print key.ljust(10), '=>', root[key]
mylist => [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
mystr => spamspamspam
mytuple => (1, 'spam', 4, 'YOU')
mydict => {'job': ('dev', 'mgr'), 'age': 42, 'name': ['Bob', 'Doe']}
Now, let's change a few of our stored persistent objects. When changing ZODB persistent class instances, in-memory attribute changes are automatically written back to the database. Other types of changes, such as in-place appends and key assignments, still require reassignment to the original key to force the change to be written to disk (built-in list and dictionary objects do not know that they are persistent): >>> rec = root['mylist'] >>> rec.append([10, 11, 12]) # change in memory >>> root['mylist'] = rec # write back to db >>> >>> rec = root['mydict'] >>> rec['age'] += 1 # change in memory >>> rec['job'] = None >>> root['mydict'] = rec # write back to db >>> import transaction >>> transaction.commit( ) >>> storage.close( ) As usual, we commit our work before exiting Python or all our changes would be lost. One more interactive session serves to verify that we've updated the database objects; there is no need to commit this time because we aren't making any changes:
...\PP3E\Database\ZODBscripts\>
python
>>>
from ZODB
import FileStorage, DB
>>>
storage = FileStorage.FileStorage(r'C:\Mark\temp\mydb.fs')
>>>
db = DB(storage)
>>>
connection = db.open( )
>>>
root = connection.root( )
>>>
>>>
print root['mylist']
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
>>>
>>>
print root['mydict']
{'job': None, 'age': 43, 'name': ['Bob', 'Doe']}
>>>
>>>
print root['mydict']['age']
43
We are essentially using Python as an interactive object database query language here; to make use of classes and scripts, let's move on to the next section. 19.6.2. Using Classes with ZODB
So far, we've been storing built-in object types such as lists and dictionaries in our ZODB databases. Such objects can handle rich information structures,
Classes used with ZODB can either be standalone, as in shelves, or derived from the ZODB Persistent class. The latter scheme provides objects with a set of precoded utility, including the ability to automatically write instance attribute changes out to the database storageno manual reassignment to root keys is required. To see how this works, let's get started by defining the class in Example 19-4: an object that records information about a bookmark in a hypothetical web site application. Example 19-4. PP3E\Database\ZODBscripts\zodb-class-make.py
Notice how this class is no longer standaloneit inherits from a ZODB superclass. In fact, unlike shelve classes, it cannot be
...\PP3E\Database\ZODBscripts> python >>> from zodb_class_make import addobjects >>> addobjects( ) ...\PP3E\Database\ZODBscripts> dir /B data class.fs class.fs.index class.fs.tmp We don't generally want to run the creation code in the top level of our process because then those classes would always have to be in the module _ _main_ _ (the name of the top-level file or the interactive prompt) each time the objects are fetched. Recall that this is a constraint of Python's pickling system discussed earlier, which underlies ZODBclasses must be reimported, and hence, located in a file in a directory on the module search path This might work if we load the class name into all our top-level scripts, with from statements, but it can be inconvenient in general. To avoid the issue, define your classes in an imported module file, and not in the main top-level script. To test database updates, Example 19-5 reads back our two stored objects and changes themany change that updates an instance attribute in memory is automatically written through to the database file. Example 19-5. PP3E\Database\ZODBscripts\zodb-class-read.py
Run this script a few times to watch the objects in your database change: the URL and modification time of one is updated, and the hit counter is modified on the other: ...\PP3E\Database\ZODBscripts\> python zodb-class-read.py pp3e url: http://www.rmi.net/~lutz/about-pp.html pp3e mod: Mon Dec 05 09:11:44 2005 ora hits: 0 ...\PP3E\Database\ZODBscripts> python zodb-class-read.py pp3e url: www.rmi.net/~lutz/about-pp3e.html pp3e mod: Mon Dec 05 09:12:12 2005 ora hits: 1 ...\PP3E\Database\ZODBscripts> python zodb-class-read.py pp3e url: www.rmi.net/~lutz/about-pp3e.html pp3e mod: Mon Dec 05 09:12:24 2005 ora hits: 2 And because these are Python objects, we can always inspect, modify, and add records interactively (be sure to also import the class to make and add a new instance): ...\PP3E\Database\ZODBscripts> c:\python24\python >>> from zodb_class_make import connectdb, mydbfile >>> root, storage = connectdb(mydbfile) >>> len(root) 2 >>> root.keys( ) ['pp3e', 'ora'] >>> root['ora'].hits 3 >>> root['pp3e'].url 'www.rmi.net/~lutz/about-pp3e.html' >>> root['ora'].hits += 1 >>> import transaction >>> transaction.commit( ) >>> storage.close( ) ...\PP3E\Database\ZODBscripts> c:\python24\python >>> from zodb_class_make import connectdb, mydbfile >>> root, storage = connectdb(mydbfile) >>> root['ora'].hits 4 19.6.3. A ZODB People Database
As a final ZODB example, let's do something a bit more realistic. If you read the
By now, we've written the usual ZODB file storage database connection logic enough times to
The net effect is that this object behaves like an automatically opened and committed database rootit provides the same interface, but adds convenience code for common use cases. You can reuse this class for any
Example 19-6. PP3E\Database\ZODBscripts\zodbtools.py
Next, the class in Example 19-7 defines the objects we'll store in our database. They are pickled as usual, but they are written out to a ZODB database, not to a shelve file. Note how this class is no longer standalone, as in our earlier shelve examplesit inherits from the ZODB Persistent class, and thus will automatically notify ZODB of changes when its instance attributes are changed. Also notice the _ _str_ _ operator overloading method here, to give a custom display format for our objects. Example 19-7. PP3E\Database\ZODBscripts\person.py
Finally, Example 19-8 tests our Person class, by creating the database and updating objects. As usual for Python's pickling system, we store the class in an imported module, not in this main, top-level script file. Otherwise, it could be reimported by Python only when class instance objects are reloaded, if it is still a part of the module _ _main_ _ ). Example 19-8. PP3E\Database\ZODBscripts\person-test.py
When run with no command-line arguments, the test script
...\PP3E\Database\ZODBscripts> python person-test.py ...\PP3E\Database\ZODBscripts> python person-test.py - bob ['bob', 'sue', 'tom'] <Person: name=sue, job=music, rate=40, pay=1600> <Engineer: name=tom, job=devel, rate=60000, pay=1153> ...\PP3E\Database\ZODBscripts> python person-test.py - bob ['bob', 'sue', 'tom'] <Person: name=sue, job=music, rate=50, pay=2000> <Engineer: name=tom.spam, job=devel, rate=65000, pay=1250> ...PP3E\Database\ZODBscripts> python person-test.py - bob ['bob', 'sue', 'tom'] <Person: name=sue, job=music, rate=60, pay=2400> <Engineer: name=tom.spam.spam, job=devel, rate=70000, pay=1346> Notice how the changeRate method updates Suethere is no need to reassign the updated record back to the original key as we have to do for shelves, because ZODB Persistent class instances are smart enough to write attribute changes to the database automatically on commits. Internally, ZODB's persistent superclasses use normal Python operator overloading to intercept attribute changes and mark the object as changed. However, direct in-place changes to mutable objects (e.g., appending to a built-in list) are not noticed by ZODB and require setting the object's _p_changed , or manual reassignment to the original key, to write changes through. ZODB also provides custom versions of some built-in mutable object types (e.g., PersistentMapping ), which write changes through automatically. 19.6.4. ZODB Resources
There are additional ZODB concepts and
For more about ZODB, search for ZODB and Zope resources on the Web. Here, let's move on to see how Python programs can make use of a very different
|