Log message for revision 29904: Synced up up with persistentclass.txt from ZODB, which was originally derived from this test.
Changed: U Zope/trunk/lib/python/ZClasses/_pmc.txt -=- Modified: Zope/trunk/lib/python/ZClasses/_pmc.txt =================================================================== --- Zope/trunk/lib/python/ZClasses/_pmc.txt 2005-04-07 23:48:40 UTC (rev 29903) +++ Zope/trunk/lib/python/ZClasses/_pmc.txt 2005-04-08 10:08:26 UTC (rev 29904) @@ -10,6 +10,8 @@ - They can only contain picklable subobjects +- They don't live in regular file-system modules + Let's look at an example: >>> def __init__(self, name): @@ -22,6 +24,7 @@ >>> class C: ... __metaclass__ = ZClasses._pmc.ZClassPersistentMetaClass ... __init__ = __init__ + ... __module__ = '__zodb__' ... foo = foo ... kind = 'sample' @@ -30,6 +33,12 @@ persistent class must be picklable. We defined the methods as global functions to make them picklable. +Also note that we explictly set the module. Persistent classes don't +live in normal Python modules. Rather, they live in the database. We +use information in __module__ to record where in the database. When +we want to use a database, we will need to supply a custom class +factory to load instances of the class. + The class we created works a lot like other persistent objects. It has standard standard persistent attributes: @@ -63,17 +72,15 @@ >>> C._p_changed False -Now, we can store the class in a database. We have to be careful, -however, to use the ZClass-aware class factory so that we can find -ZClasses, which are stored in the database, rather than in modules: +Now, we can store the class in a database. We're going to use an +explicit transaction manager so that we can show parallel transactions +without having to use threads. - >>> import Zope2.App.ClassFactory - >>> some_database.classFactory = Zope2.App.ClassFactory.ClassFactory - - >>> connection = some_database.open() + >>> import transaction + >>> tm = transaction.TransactionManager() + >>> connection = some_database.open(txn_mgr=tm) >>> connection.root()['C'] = C - >>> import transaction - >>> transaction.commit() + >>> tm.commit() Now, if we look at the persistence variables, we'll see that they have values: @@ -102,7 +109,7 @@ If we abort the transaction: - >>> transaction.abort() + >>> tm.abort() Then the class will return to it's prior state: @@ -114,23 +121,15 @@ >>> c.bar() bar first -We can open another connection and access the class there. Let's do -that in another thread: - >>> import threading - >>> def run(func): - ... thread = threading.Thread(target=func) - ... thread.start() - ... thread.join() +We can open another connection and access the class there. - >>> def read_class(): - ... connection = some_database.open() - ... C = connection.root()['C'] - ... c = C('other') - ... c.bar() - ... connection.close() + >>> tm2 = transaction.TransactionManager() + >>> connection2 = some_database.open(txn_mgr=tm2) - >>> run(read_class) + >>> C2 = connection2.root()['C'] + >>> c2 = C2('other') + >>> c2.bar() bar other If we make changes without commiting them: @@ -139,28 +138,27 @@ >>> c.bar() baz first -Other connections/threads are unaffected: + >>> C is C2 + False - >>> run(read_class) +Other connections are unaffected: + + >>> connection2.sync() + >>> c2.bar() bar other Until we commit: - >>> transaction.commit() - >>> run(read_class) + >>> tm.commit() + >>> connection2.sync() + >>> c2.bar() baz other -Similarly, we don't see changes made in other connextions: +Similarly, we don't see changes made in other connections: - >>> def write_class(): - ... connection = some_database.open() - ... C = connection.root()['C'] - ... C.color = 'red' - ... transaction.commit() - ... connection.close() + >>> C2.color = 'red' + >>> tm2.commit() - >>> run(write_class) - >>> c.color Traceback (most recent call last): ... @@ -172,3 +170,114 @@ >>> c.color 'red' +Instances of Persistent Classes +------------------------------- + +We can, of course, store instances of perstent classes in the +database: + + >>> c.color = 'blue' + >>> connection.root()['c'] = c + >>> tm.commit() + + >>> connection2.sync() + >>> connection2.root()['c'].color + 'blue' + +NOTE: If a non-persistent instance of a persistent class is copied, + the class may be copied as well. This is usually not the desired + result. + + +Persistent instances of persistent classes +------------------------------------------ + +Persistent instances of persistent classes are handled differently +than normal instances. When we copy a persistent instances of a +persistent class, we want to avoid copying the class. + +Lets create a persistent class that subclasses Persistent: + + >>> import persistent + >>> class P(persistent.Persistent, C): + ... __module__ = '__zodb__' + ... color = 'green' + + >>> connection.root()['P'] = P + + >>> import persistent.mapping + >>> connection.root()['obs'] = persistent.mapping.PersistentMapping() + >>> p = P('p') + >>> connection.root()['obs']['p'] = p + >>> tm.commit() + +You might be wondering why we didn't just stick 'p' into the root +object. We created an intermediate persistent object instead. We are +storing persistent classes in the root object. To create a ghost for a +persistent instance of a persistent class, we need to be able to be +able to access the root object and it must be loaded first. If the +instance was in the root object, we'd be unable to create it while +loading the root object. + +Now, if we try to load it, we get a broken oject: + + >>> connection2.sync() + >>> connection2.root()['obs']['p'] + <persistent broken __zodb__.P instance '\x00\x00\x00\x00\x00\x00\x00\x04'> + +because the module, "__zodb__" can't be loaded. We need to provide a +class factory that knows about this special module. Here we'll supply a +sample class factory that looks up a class name in the database root +if the module is "__zodb__". It falls back to the normal class lookup +for other modules: + + >>> from ZODB.broken import find_global + >>> def classFactory(connection, modulename, globalname): + ... if modulename == '__zodb__': + ... return connection.root()[globalname] + ... return find_global(modulename, globalname) + + >>> some_database.classFactory = classFactory + +Normally, the classFactory should be set before a database is opened. +We'll reopen the connections we're using. We'll assign the old +connections to a variable first to prevent getting them from the +connection pool: + + >>> old = connection, connection2 + >>> connection = some_database.open(txn_mgr=tm) + >>> connection2 = some_database.open(txn_mgr=tm2) + +Now, we can read the object: + + >>> connection2.root()['obs']['p'].color + 'green' + >>> connection2.root()['obs']['p'].color = 'blue' + >>> tm2.commit() + + >>> connection.sync() + >>> p = connection.root()['obs']['p'] + >>> p.color + 'blue' + +Copying +------- + +If we copy an instance via export/import, the copy and the original +share the same class: + + >>> file = connection.exportFile(p._p_oid) + >>> file.seek(0) + >>> cp = connection.importFile(file) + >>> cp.color + 'blue' + + >>> cp is not p + True + + >>> cp.__class__ is p.__class__ + True + + + +XXX test abort of import _______________________________________________ Zope-Checkins maillist - Zope-Checkins@zope.org http://mail.zope.org/mailman/listinfo/zope-checkins