Commit d8a161cb authored by Jim Fulton's avatar Jim Fulton

Added cross-database-reference support.

parent fc3e142c
Cross-Database References
=========================
Persistent references to objects in different databases within a
multi-database are allowed.
Lets set up a multi-database with 2 databases:
>>> import ZODB.tests.util, transaction, persistent
>>> databases = {}
>>> db1 = ZODB.tests.util.DB(databases=databases, database_name='1')
>>> db2 = ZODB.tests.util.DB(databases=databases, database_name='2')
And create a persistent object in the first database:
>>> tm = transaction.TransactionManager()
>>> conn1 = db1.open(transaction_manager=tm)
>>> p1 = MyClass()
>>> conn1.root()['p'] = p1
>>> tm.commit()
Now, we'll create a second persistent object in the second database.
We'll have a reference to the first object:
>>> tm = transaction.TransactionManager()
>>> conn2 = db2.open(transaction_manager=tm)
>>> p2 = MyClass()
>>> conn2.root()['p'] = p2
>>> p2.p1 = p1
>>> tm.commit()
Now, let's open a separate connection to database 2. We use it to
read p2, use p2 to get to p1, and verify that it is in database 1:
>>> conn = db2.open()
>>> p2x = conn.root()['p']
>>> p1x = p2x.p1
>>> p2x is p2, p2x._p_oid == p2._p_oid, p2x._p_jar.db() is db2
(False, True, True)
>>> p1x is p1, p1x._p_oid == p1._p_oid, p1x._p_jar.db() is db1
(False, True, True)
It isn't valid to create references outside a multi database:
>>> db3 = ZODB.tests.util.DB()
>>> conn3 = db3.open(transaction_manager=tm)
>>> p3 = MyClass()
>>> conn3.root()['p'] = p3
>>> tm.commit()
>>> p2.p3 = p3
>>> tm.commit() # doctest: +NORMALIZE_WHITESPACE
Traceback (most recent call last):
...
InvalidObjectReference:
Attempt to store an object from a foreign database connection
NOTE
----
This implementation is incomplete. It allows creatting and using
cross-database references, however, there are a number of facilities
missing:
cross-database garbage collection
Garbage collection is done on a database by database basis.
If an object on a database only has references to it from other
databases, then the object will be garbage collected when it's
database is packed. The cross-database references to it will be
broken.
cross-database undo
Undo is only applied to a single database. Fixing this for
mutliple databases is going to be extremely difficult. Undo
currently poses consistency problems, so it is not (or should not
be) widely used.
Cross-database aware (tolerant) export/import
The export/import facility needs to be aware, at least, of cross-database
references.
...@@ -188,6 +188,10 @@ class ObjectWriter: ...@@ -188,6 +188,10 @@ class ObjectWriter:
>>> class DummyJar: >>> class DummyJar:
... def new_oid(self): ... def new_oid(self):
... return 42 ... return 42
... def db(self):
... return self
... databases = {}
>>> jar = DummyJar() >>> jar = DummyJar()
>>> class O: >>> class O:
... _p_jar = jar ... _p_jar = jar
...@@ -312,15 +316,26 @@ class ObjectWriter: ...@@ -312,15 +316,26 @@ class ObjectWriter:
# NOTE! Persistent classes don't (and can't) subclass persistent. # NOTE! Persistent classes don't (and can't) subclass persistent.
database_name = None
if oid is None: if oid is None:
oid = obj._p_oid = self._jar.new_oid() oid = obj._p_oid = self._jar.new_oid()
obj._p_jar = self._jar obj._p_jar = self._jar
self._stack.append(obj) self._stack.append(obj)
elif obj._p_jar is not self._jar: elif obj._p_jar is not self._jar:
raise InvalidObjectReference(
"Attempt to store an object from a foreign " try:
"database connection" otherdb = obj._p_jar.db()
) database_name = otherdb.database_name
except AttributeError:
otherdb = self
if self._jar.db().databases.get(database_name) is not otherdb:
raise InvalidObjectReference(
"Attempt to store an object from a foreign "
"database connection"
)
klass = type(obj) klass = type(obj)
if hasattr(klass, '__getnewargs__'): if hasattr(klass, '__getnewargs__'):
...@@ -333,11 +348,15 @@ class ObjectWriter: ...@@ -333,11 +348,15 @@ class ObjectWriter:
# __getnewargs__ of its own, we'll lose the optimization # __getnewargs__ of its own, we'll lose the optimization
# of caching the class info. # of caching the class info.
if database_name:
return ['n', (database_name, oid)]
return oid return oid
# Note that we never get here for persistent classes. # Note that we never get here for persistent classes.
# We'll use driect refs for normal classes. # We'll use driect refs for normal classes.
if database_name:
return ['m', (database_name, oid, klass)]
return oid, klass return oid, klass
def serialize(self, obj): def serialize(self, obj):
...@@ -475,7 +494,14 @@ class ObjectReader: ...@@ -475,7 +494,14 @@ class ObjectReader:
self._cache[oid] = obj self._cache[oid] = obj
return obj return obj
loaders['p'] = load_persistent def load_multi_persistent(self, database_name, oid, klass):
conn = self._conn.get_connection(database_name)
# TODO, make connection _cache attr public
reader = ObjectReader(conn, conn._cache, self._factory)
return reader.load_persistent(oid, klass)
loaders['m'] = load_multi_persistent
def load_persistent_weakref(self, oid): def load_persistent_weakref(self, oid):
obj = WeakRef.__new__(WeakRef) obj = WeakRef.__new__(WeakRef)
...@@ -491,7 +517,13 @@ class ObjectReader: ...@@ -491,7 +517,13 @@ class ObjectReader:
return obj return obj
return self._conn.get(oid) return self._conn.get(oid)
loaders['o'] = load_oid def load_multi_oid(self, database_name, oid):
conn = self._conn.get_connection(database_name)
# TODO, make connection _cache attr public
reader = ObjectReader(conn, conn._cache, self._factory)
return reader.load_oid(oid)
loaders['n'] = load_multi_oid
def _new_object(self, klass, args): def _new_object(self, klass, args):
if not args and not myhasattr(klass, "__getnewargs__"): if not args and not myhasattr(klass, "__getnewargs__"):
......
##############################################################################
#
# Copyright (c) 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""
$Id$
"""
import unittest
from zope.testing import doctest
import persistent
class MyClass(persistent.Persistent):
pass
class MyClass_w_getnewargs(persistent.Persistent):
def __getnewargs__(self):
return ()
def test_suite():
return unittest.TestSuite((
doctest.DocFileSuite('../cross-database-references.txt',
globs=dict(MyClass=MyClass),
),
doctest.DocFileSuite('../cross-database-references.txt',
globs=dict(MyClass=MyClass_w_getnewargs),
),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment