Michael Bayer wrote:
dmiller wrote:
2. Switch the active UOW each time a window gains focus (error prone,
messy)
Actually this is probably your best option. because, its not just passing
the UOW to all explicit mapper operations, the UOW is used every time you
set an attribute on an object or append/delete from a list attribute.
If your "sessions" really do correspond with "window focus" then you would
have to match them up that way. Just like the widgets on the screen
correspond to the window, so the individual data objects they represent
correspond to the UOW. I gather your object-modify and commit()
operations are occuring within an event loop ? Maybe you could extend the
event-loop system itself to always insure the correct UOW is selected ?
I ended up extending the sqlalchemy.objectstore.uow (i.e. ScopedRegistry) to support a new kind of
scope (session). I also needed an "objectstore" that would always resolve to a given
UnitOfWork (rather than using the normal scoped UnitOfWork resolution process). For that I wrote an
"ObjectStore" class that I could use to manage a single UnitOfWork. I think this would be
a valuable addition to SQLAlchemy.
The attached patch contains the new ObjectStore class as well as a refactored version of
ScopedRegistry which supports adding arbitrary scopes through a new "add_scope"
method.
~ Daniel
Index: lib/sqlalchemy/mapping/objectstore.py
===================================================================
--- lib/sqlalchemy/mapping/objectstore.py (revision 931)
+++ lib/sqlalchemy/mapping/objectstore.py (working copy)
@@ -26,104 +26,147 @@
# with the "echo_uow=True" keyword argument.
LOG = False
-def get_id_key(ident, class_, table):
- """returns an identity-map key for use in storing/retrieving an item from
the identity
- map, given a tuple of the object's primary key values.
+class ObjectStore(object):
- ident - a tuple of primary key values corresponding to the object to be
stored. these
- values should be in the same order as the primary keys of the table
-
- class_ - a reference to the object's class
+ def __init__(self, registry):
+ """Initialize the objectstore with a UnitOfWork registry"""
+ self.registry = registry
- table - a Table object where the object's primary fields are stored.
+ def get_id_key(ident, class_, table):
+ """returns an identity-map key for use in storing/retrieving an item
from the identity
+ map, given a tuple of the object's primary key values.
- selectable - a Selectable object which represents all the object's
column-based fields.
- this Selectable may be synonymous with the table argument or can be a
larger construct
- containing that table. return value: a tuple object which is used as an
identity key. """
- return (class_, table.hash_key(), tuple(ident))
-def get_row_key(row, class_, table, primary_key):
- """returns an identity-map key for use in storing/retrieving an item from
the identity
- map, given a result set row.
+ ident - a tuple of primary key values corresponding to the object to
be stored. these
+ values should be in the same order as the primary keys of the table
- row - a sqlalchemy.dbengine.RowProxy instance or other map corresponding
result-set
- column names to their values within a row.
+ class_ - a reference to the object's class
- class_ - a reference to the object's class
+ table - a Table object where the object's primary fields are stored.
- table - a Table object where the object's primary fields are stored.
+ selectable - a Selectable object which represents all the object's
column-based fields.
+ this Selectable may be synonymous with the table argument or can be a
larger construct
+ containing that table. return value: a tuple object which is used as
an identity key. """
+ return (class_, table.hash_key(), tuple(ident))
+ get_id_key = staticmethod(get_id_key)
- selectable - a Selectable object which represents all the object's
column-based fields.
- this Selectable may be synonymous with the table argument or can be a
larger construct
- containing that table. return value: a tuple object which is used as an
identity key.
- """
- return (class_, table.hash_key(), tuple([row[column] for column in
primary_key]))
+ def get_row_key(row, class_, table, primary_key):
+ """returns an identity-map key for use in storing/retrieving an item
from the identity
+ map, given a result set row.
+ row - a sqlalchemy.dbengine.RowProxy instance or other map
corresponding result-set
+ column names to their values within a row.
+
+ class_ - a reference to the object's class
+
+ table - a Table object where the object's primary fields are stored.
+
+ selectable - a Selectable object which represents all the object's
column-based fields.
+ this Selectable may be synonymous with the table argument or can be a
larger construct
+ containing that table. return value: a tuple object which is used as
an identity key.
+ """
+ return (class_, table.hash_key(), tuple([row[column] for column in
primary_key]))
+ get_row_key = staticmethod(get_row_key)
+
+ def begin(self):
+ """begins a new UnitOfWork transaction. the next commit will affect
only
+ objects that are created, modified, or deleted following the begin
statement."""
+ self.registry().begin()
+
+ def commit(self, *obj):
+ """commits the current UnitOfWork transaction. if a transaction was
begun
+ via begin(), commits only those objects that were created, modified,
or deleted
+ since that begin statement. otherwise commits all objects that have
been
+ changed."""
+ self.registry().commit(*obj)
+
+ def clear(self):
+ """removes all current UnitOfWorks and IdentityMaps for this thread
and
+ establishes a new one. It is probably a good idea to discard all
+ current mapped object instances, as they are no longer in the Identity
Map."""
+ self.registry.clear()
+
+ def delete(self, *obj):
+ """registers the given objects as to be deleted upon the next commit"""
+ uw = self.registry()
+ for o in obj:
+ uw.register_deleted(o)
+
+ def has_key(self, key):
+ """returns True if the current thread-local IdentityMap contains the
given instance key"""
+ return self.registry().identity_map.has_key(key)
+
+ def has_instance(self, instance):
+ """returns True if the current thread-local IdentityMap contains the
given instance"""
+ return self.registry().identity_map.has_key(instance_key(instance))
+
+ def is_dirty(self, obj):
+ """returns True if the given object is in the current UnitOfWork's new
or dirty list,
+ or if its a modified list attribute on an object."""
+ return self.registry().is_dirty(obj)
+
+ def instance_key(instance):
+ """returns the IdentityMap key for the given instance"""
+ return object_mapper(instance).instance_key(instance)
+ instance_key = staticmethod(instance_key)
+
+ def import_instance(self, instance):
+ """places the given instance in the current thread's unit of work
context,
+ either in the current IdentityMap or marked as "new". Returns either
the object
+ or the current corresponding version in the Identity Map.
+
+ this method should be used for any object instance that is coming from
a serialized
+ storage, from another thread (assuming the regular threaded unit of
work model), or any
+ case where the instance was loaded/created corresponding to a
different base unitofwork
+ than the current one."""
+ if instance is None:
+ return None
+ key = getattr(instance, '_instance_key', None)
+ mapper = object_mapper(instance)
+ key = (key[0], mapper.table.hash_key(), key[2])
+ u = self.registry()
+ if key is not None:
+ if u.identity_map.has_key(key):
+ return u.identity_map[key]
+ else:
+ instance._instance_key = key
+ u.identity_map[key] = instance
+ else:
+ u.register_new(instance)
+ return instance
+
+def get_id_key(ident, class_, table):
+ return objectstore.get_id_key(ident, class_, table)
+
+def get_row_key(row, class_, table, primary_key):
+ return objectstore.get_row_key(row, class_, table, primary_key)
+
def begin():
- """begins a new UnitOfWork transaction. the next commit will affect only
- objects that are created, modified, or deleted following the begin
statement."""
- uow().begin()
-
+ objectstore.begin()
+
def commit(*obj):
- """commits the current UnitOfWork transaction. if a transaction was begun
- via begin(), commits only those objects that were created, modified, or
deleted
- since that begin statement. otherwise commits all objects that have been
- changed."""
- uow().commit(*obj)
-
+ objectstore.commit(*obj)
+
def clear():
- """removes all current UnitOfWorks and IdentityMaps for this thread and
- establishes a new one. It is probably a good idea to discard all
- current mapped object instances, as they are no longer in the Identity
Map."""
- uow.set(UnitOfWork())
+ objectstore.clear()
def delete(*obj):
- """registers the given objects as to be deleted upon the next commit"""
- uw = uow()
- for o in obj:
- uw.register_deleted(o)
-
+ objectstore.delete(*obj)
+
def has_key(key):
- """returns True if the current thread-local IdentityMap contains the given
instance key"""
- return uow().identity_map.has_key(key)
+ return objectstore.has_key(key)
def has_instance(instance):
- """returns True if the current thread-local IdentityMap contains the given
instance"""
- return uow().identity_map.has_key(instance_key(instance))
+ return objectstore.has_instance(instance)
def is_dirty(obj):
- """returns True if the given object is in the current UnitOfWork's new or
dirty list,
- or if its a modified list attribute on an object."""
- return uow().is_dirty(obj)
-
+ return objectstore.is_dirty(obj)
+
def instance_key(instance):
- """returns the IdentityMap key for the given instance"""
- return object_mapper(instance).instance_key(instance)
+ return objectstore.instance_key(instance)
def import_instance(instance):
- """places the given instance in the current thread's unit of work context,
- either in the current IdentityMap or marked as "new". Returns either the
object
- or the current corresponding version in the Identity Map.
-
- this method should be used for any object instance that is coming from a
serialized
- storage, from another thread (assuming the regular threaded unit of work
model), or any
- case where the instance was loaded/created corresponding to a different
base unitofwork
- than the current one."""
- if instance is None:
- return None
- key = getattr(instance, '_instance_key', None)
- mapper = object_mapper(instance)
- key = (key[0], mapper.table.hash_key(), key[2])
- u = uow()
- if key is not None:
- if u.identity_map.has_key(key):
- return u.identity_map[key]
- else:
- instance._instance_key = key
- u.identity_map[key] = instance
- else:
- u.register_new(instance)
- return instance
-
+ return objectstore.import_instance(instance)
+
class UOWListElement(attributes.ListElement):
def __init__(self, obj, key, data=None, deleteremoved=False, **kwargs):
attributes.ListElement.__init__(self, obj, key, data=data, **kwargs)
@@ -908,4 +951,5 @@
return sqlalchemy.mapperlib.object_mapper(obj)
global_attributes = UOWAttributeManager()
-uow = util.ScopedRegistry(lambda: UnitOfWork(), "thread")
+objectstore = ObjectStore(registry=util.ScopedRegistry(UnitOfWork, "thread"))
+uow = objectstore.registry # Note: this is not a UnitOfWork, it is a
ScopedRegistry that manages UnitOfWork objects
Index: lib/sqlalchemy/util.py
===================================================================
--- lib/sqlalchemy/util.py (revision 931)
+++ lib/sqlalchemy/util.py (working copy)
@@ -357,58 +357,47 @@
class ScopedRegistry(object):
"""a Registry that can store one or multiple instances of a single class
- on a per-application or per-thread scoped basis"""
+ on a per-application or per-thread scoped basis
+
+ createfunc - a callable that returns a new object to be placed in the
registry
+ defaultscope - the default scope to be used ('application', 'thread', or
'session')
+ """
def __init__(self, createfunc, defaultscope):
self.createfunc = createfunc
self.defaultscope = defaultscope
- self.application = createfunc()
- self.threadlocal = {}
self.scopes = {
- 'application' : {'call' : self._call_application, 'clear' :
self._clear_application, 'set':self._set_application},
- 'thread' : {'call' : self._call_thread,
'clear':self._clear_thread, 'set':self._set_thread}
- }
+ "application": lambda:None,
+ "thread": thread.get_ident,
+ }
+ self.registry = {}
+ def add_scope(self, scope, keyfunc, default=True):
+ self.scopes[scope] = keyfunc
+ if default:
+ self.defaultscope = scope
+
def __call__(self, scope = None):
- if scope is None:
- scope = self.defaultscope
- return self.scopes[scope]['call']()
+ key = self._get_key(scope)
+ try:
+ return self.registry[key]
+ except KeyError:
+ return self.registry.setdefault(key, self.createfunc())
def set(self, obj, scope = None):
- if scope is None:
- scope = self.defaultscope
- return self.scopes[scope]['set'](obj)
+ self.registry[self._get_key(scope)] = obj
def clear(self, scope = None):
- if scope is None:
- scope = self.defaultscope
- return self.scopes[scope]['clear']()
-
- def _set_thread(self, obj):
- self.threadlocal[thread.get_ident()] = obj
-
- def _call_thread(self):
try:
- return self.threadlocal[thread.get_ident()]
+ del self.registry[self._get_key(scope)]
except KeyError:
- return self.threadlocal.setdefault(thread.get_ident(),
self.createfunc())
-
- def _clear_thread(self):
- try:
- del self.threadlocal[thread.get_ident()]
- except KeyError:
pass
- def _set_application(self, obj):
- self.application = obj
-
- def _call_application(self):
- return self.application
+ def _get_key(self, scope):
+ if scope is None:
+ scope = self.defaultscope
+ return (scope, self.scopes[scope]())
- def _clear_application(self):
- self.application = createfunc()
-
-
def constructor_args(instance, **kwargs):
classobj = instance.__class__