Michael Bayer wrote:
I have committed a preliminary version of this change, which makes the
central point of access within objectstore the "session", i.e.:
s = objectstore.session()
I think the implementation is still more complicated than it needs to be. Here
are some thoughts and a patch to make it simpler.
It was confusing to me that objectstore.uow was a function. The same goes for objectstore.session. It looks
like an instance. The problem here is conventions: though objectstore is a module it is presented as an
object in the documentation. Accordingly, objectstore.session looks like a property that returns a session
object when accessed. However, it's a function that returns a Session instance when called (that's
confusing). To make this more intuitive I think we should rename "session(...)" to
"get_session(...)" (leave uow like it is because that's legacy now). The "get_" prefix
implies that it's a function not a property (attribute) of objectstore.
I noticed that you're doing a thread-local thing with sessions. That's fine. Let's make
Session the main access point for UnitOfWork functionality. UnitOfWork becomes an
implementation detail that is not directly exposed to (most) SA users. In addition, the
Session manages UnitOfWork instances with respect to the UnitOfWork begin/commit cycle.
UnitOfWork should not know about the Session (or any type of session scoping
mechanism)--it is a unit of work, not a session manager. The logic in
UnitOfWork.begin/commit (that creates a "child" UnitOfWork and places it in the
registry) moves into Session, which is much cleaner. Aside: it is up to the user to use
thread-safe programming practices when using Session objects directly. This should be
simple now that Session is a stateful object that does not know (or care) about the
thread-local pattern internally.
objectstore.get_session now provides access to a Session manager that (by
default) uses a thread-local pattern. This is a nice feature provided by SA,
but should not be tightly coupled with the more powerful Session/UnitOfWork
features. Thread-local session is a common ORM data access pattern. However, it
is only one of many possible patterns. Before Session became a full-blown
object, SQLAlchemy did not allow other more complicated patterns to be used
without totally replacing the UnitOfWork management logic provided by default.
High level overview of sqlalchemy.objectstore:
UnitOfWork - central SQLAlchemy object manager
- does not know about Session objects (Session manages UnitOfWork, not the
reverse)
Session - proxy for UnitOfWork. manages UnitOfWork begin/commit cycle
- does not know about session_registry or get_session
- when passing objects between threads, it is recommended to "import_instance"
into a thread-local session (Session behavior is undefined if this recommendation is not
observed).
session_registry - ScopedRegistry providing scoped access to SA-managed Session
objects
- defaults to thread-local session pattern
- the only reason a user would ever access this is to change the default Session scope from
"thread" to "application"
- does not directly deal with UnitOfWork instances (Session takes care of that)
_sessions - private (weak value ref) dictionary of all Session instances
created by anyone
- provides a mechanism for a SQLAlchemy-managed object to weakly reference a
session (no more, no less)
get_session(obj=None, scope=None) - gets the session associated with the given
object or the scoped session (if obj is given, scope has no affect. if neither
is given, gets a session in the default scope)
- is this too complicated? I'd really like it to do just one thing
- does not directly deal with UnitOfWork instances (Session takes care of that)
uow - deprecated (use get_session)
- objectstore.uow.scope is now objectstore.session_registry.scope
using_session(session, func) - execute func with session default scoped session
A patch that implements most of this is attached.
~ Daniel
Index: lib/sqlalchemy/mapping/mapper.py
===================================================================
--- lib/sqlalchemy/mapping/mapper.py (revision 942)
+++ lib/sqlalchemy/mapping/mapper.py (working copy)
@@ -205,7 +205,7 @@
oldinit = self.class_.__init__
def init(self, *args, **kwargs):
nohist = kwargs.pop('_mapper_nohistory', False)
- session = kwargs.pop('_sa_session', objectstore.session())
+ session = kwargs.pop('_sa_session', objectstore.get_session())
if oldinit is not None:
try:
oldinit(self, *args, **kwargs)
@@ -246,7 +246,7 @@
# store new stuff in the identity map
for value in imap.values():
- objectstore.session().register_clean(value)
+ objectstore.get_session().register_clean(value)
if len(mappers):
return [result] + otherresults
@@ -263,7 +263,7 @@
def _get(self, key, ident=None):
try:
- return objectstore.session()._get(key)
+ return objectstore.get_session()._get(key)
except KeyError:
if ident is None:
ident = key[2]
@@ -680,8 +680,8 @@
# including modifying any of its related items lists, as its already
# been exposed to being modified by the application.
identitykey = self._identity_key(row)
- if objectstore.session().has_key(identitykey):
- instance = objectstore.session()._get(identitykey)
+ if objectstore.get_session().has_key(identitykey):
+ instance = objectstore.get_session()._get(identitykey)
isnew = False
if populate_existing:
Index: lib/sqlalchemy/mapping/objectstore.py
===================================================================
--- lib/sqlalchemy/mapping/objectstore.py (revision 942)
+++ lib/sqlalchemy/mapping/objectstore.py (working copy)
@@ -27,30 +27,25 @@
LOG = False
class Session(object):
- """a scope-managed proxy to UnitOfWork instances. Operations are delegated
- to UnitOfWork objects which are accessed via a
sqlalchemy.util.ScopedRegistry object.
- The registry is capable of maintaining object instances on a thread-local,
- per-application, or custom user-defined basis."""
+ """a proxy to UnitOfWork."""
- def __init__(self, uow=None, registry=None, hash_key=None):
+ def __init__(self, uowfunc=None, hash_key=None):
"""Initialize the objectstore with a UnitOfWork registry. If called
with no arguments, creates a single UnitOfWork for all operations.
- registry - a sqlalchemy.util.ScopedRegistry to produce UnitOfWork
instances.
- This argument should not be used with the uow argument.
- uow - a UnitOfWork to use for all operations. this argument should
not be
- used with the registry argument.
- hash_key - the hash_key used to identify objects against this session,
which
+ uowfunc - (optional) a callable that returns a clean UnitOfWork when
called. It
+ must accept one optional "parent" argument
+ hash_key - (optional) the hash_key used to identify objects against
this session.
defaults to the id of the Session instance.
-
"""
- if registry is None:
- if uow is None:
- uow = UnitOfWork(self)
- self.registry = util.ScopedRegistry(lambda:uow, 'application')
+ if uowfunc is None:
+ uowfunc = UnitOfWork
+ self.uowfunc = uowfunc
+ self.uow = uowfunc()
+ if hash_key is None:
+ self.hash_key = id(self)
else:
- self.registry = registry
- self._hash_key = hash_key
+ self.hash_key = hash_key
def get_id_key(ident, class_, table):
"""returns an identity-map key for use in storing/retrieving an item
from the identity
@@ -87,35 +82,54 @@
return (class_, table.hash_key(), tuple([row[column] for column in
primary_key]))
get_row_key = staticmethod(get_row_key)
- def _set_uow(self, uow):
- self.registry.set(uow)
- uow = property(lambda s:s.registry(), _set_uow, doc="Returns a
scope-specific UnitOfWork object for this session.")
-
- hash_key = property(lambda s:s._hash_key or id(s))
-
- def bind_to(self, obj):
- """given an object, binds it to this session. changes on the object
will affect
- the currently scoped UnitOfWork maintained by this session."""
- obj._sa_session_id = self.hash_key
-
def __getattr__(self, key):
"""proxy other methods to our underlying UnitOfWork"""
- return getattr(self.registry(), key)
+ return getattr(self.uow, 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.uow = self.uowfunc(parent=self.uow)
+
+ def commit(self):
+ """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."""
+ if self.uow.parent is not None:
+ self.uow.commit()
+ self.uow = self.uow.parent
+ else:
+ self.uow.commit()
+
def clear(self):
- self.registry.clear()
+ """removes all current UnitOfWork instances from this session 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.uow = self.uowfunc()
+ def rollback(self):
+ if self.uow.parent is None:
+ raise "UOW transaction is not begun"
+ #self.uow.rollback() ??
+ self.uow = self.uow.parent
+
def delete(*obj):
"""registers the given objects as to be deleted upon the next commit"""
- u = registry()
+ u = self.uow
for o in obj:
u.register_deleted(o)
+ def bind_to(self, obj):
+ """given an object, binds it to this session. changes on the object
will affect
+ the currently scoped UnitOfWork maintained by this session."""
+ obj._sa_session_id = self.hash_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
@@ -125,7 +139,7 @@
key = getattr(instance, '_instance_key', None)
mapper = object_mapper(instance)
key = (key[0], mapper.table.hash_key(), key[2])
- u = self.registry()
+ u = self.uow
if key is not None:
if u.identity_map.has_key(key):
return u.identity_map[key]
@@ -137,6 +151,13 @@
u.register_new(instance)
return instance
+ def register_clean(self, obj):
+ self.bind_to(obj)
+ self.uow.register_clean(obj)
+
+ def register_new(self, obj):
+ self.bind_to(obj)
+ self.uow.register_new(obj)
def get_id_key(ident, class_, table):
return Session.get_id_key(ident, class_, table)
@@ -147,53 +168,53 @@
def begin():
"""begins a new UnitOfWork transaction. the next commit will affect only
objects that are created, modified, or deleted following the begin
statement."""
- session().begin()
+ get_session().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."""
- session().commit(*obj)
+ get_session().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."""
- session().clear()
+ get_session().clear()
def delete(*obj):
"""registers the given objects as to be deleted upon the next commit"""
- s = session()
+ s = get_session()
for o in obj:
s.register_deleted(o)
def has_key(key):
"""returns True if the current thread-local IdentityMap contains the given
instance key"""
- return session().has_key(key)
+ return get_session().has_key(key)
def has_instance(instance):
"""returns True if the current thread-local IdentityMap contains the given
instance"""
- return session().has_instance(instance)
+ return get_session().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 session().is_dirty(obj)
+ return get_session().is_dirty(obj)
def instance_key(instance):
"""returns the IdentityMap key for the given instance"""
- return session().instance_key(instance)
+ return get_session().instance_key(instance)
def import_instance(instance):
- return session().import_instance(instance)
+ return get_session().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)
self.deleteremoved = deleteremoved
def list_value_changed(self, obj, key, item, listval, isdelete):
- sess = session(obj)
+ sess = get_session(obj)
if not isdelete and sess.deleted.contains(item):
raise "re-inserting a deleted value into a list"
sess.modified_lists.append(self)
@@ -211,21 +232,15 @@
def value_changed(self, obj, key, value):
if hasattr(obj, '_instance_key'):
- session(obj).register_dirty(obj)
+ get_session(obj).register_dirty(obj)
else:
- session(obj).register_new(obj)
+ get_session(obj).register_new(obj)
def create_list(self, obj, key, list_, **kwargs):
return UOWListElement(obj, key, list_, **kwargs)
class UnitOfWork(object):
- def __init__(self, session, parent=None, is_begun=False):
- self.session = session
- self.is_begun = is_begun
- if is_begun:
- self.begin_count = 1
- else:
- self.begin_count = 0
+ def __init__(self, parent=None):
if parent is not None:
self.identity_map = parent.identity_map
else:
@@ -300,12 +315,10 @@
if not hasattr(obj, '_instance_key'):
mapper = object_mapper(obj)
obj._instance_key = mapper.instance_key(obj)
- self.session.bind_to(obj)
self._put(obj._instance_key, obj)
self.attributes.commit(obj)
def register_new(self, obj):
- self.session.bind_to(obj)
self.new.append(obj)
def register_dirty(self, obj):
@@ -331,18 +344,8 @@
pass
# TODO: tie in register_new/register_dirty with table transaction begins ?
- def begin(self):
- if self.is_begun:
- self.begin_count += 1
- return
- u = UnitOfWork(self.session, parent=self, is_begun=True)
- self.session.registry.set(u)
def commit(self, *objects):
- if self.is_begun:
- self.begin_count -= 1
- if self.begin_count > 0:
- return
commit_context = UOWTransaction(self)
if len(objects):
@@ -396,20 +399,15 @@
e.commit()
commit_context.post_exec()
-
- if self.parent:
- self.session.registry.set(self.parent)
def rollback_object(self, obj):
self.attributes.rollback(obj)
def rollback(self):
- if not self.is_begun:
- raise "UOW transaction is not begun"
# roll back attributes ? nah....
#for obj in self.deleted + self.dirty + self.new:
# self.attributes.rollback(obj)
- self.session.registry.set(self.parent)
+ pass
class UOWTransaction(object):
"""handles the details of organizing and executing transaction tasks
@@ -977,41 +975,47 @@
global_attributes = UOWAttributeManager()
-thread_session = Session(registry=util.ScopedRegistry(lambda:
UnitOfWork(thread_session), "thread"), hash_key='thread')
-uow = thread_session.registry # Note: this is not a UnitOfWork, it is a
ScopedRegistry that manages UnitOfWork objects
+session_registry = util.ScopedRegistry(Session, "thread") # Default session
registry
-_sessions = weakref.WeakValueDictionary()
-_sessions[thread_session.hash_key] = thread_session
+_sessions = weakref.WeakValueDictionary() # all referenced sessions (including
user-created)
-def session(obj=None):
- # object-specific session ?
+def get_session(obj=None, scope=None):
+ """returns a Session object
+
+ obj - if provided, the session referenced by this object will be returned.
+ If the an associated session cannot be found the scoped session will be
+ returned.
+ scope - if provided, the session in this scope will be returned
+
+ WARNING: this function may return a session from a different thread if obj
+ is not associated with a thread-local session (use Session.import_instance
+ to ensure that this does not happen)
+ """
if obj is not None:
- # does it have a hash key ?
hashkey = getattr(obj, '_sa_session_id', None)
if hashkey is not None:
- # ok, return that
try:
return _sessions[hashkey]
except KeyError:
- # oh, its gone, nevermind
pass
+ return session_registry(scope=scope)
+
+def uow():
+ print "WARNING: objectstore.uow is deprecated. use objectstore.get_session"
+ return get_session().uow
- try:
- # have a thread-locally defined session (via using_session) ?
- return _sessions[thread.get_ident()]
- except KeyError:
- # nope, return the regular session
- return thread_session
+def using_session(session, func):
+ """call func with the given session as the default-scoped session
-def using_session(sess, func):
- old = _sessions.get(thread.get_ident(), None)
+ Note: the default-scoped session will always be restored even if func
+ raises an exception.
+ """
+ old = session_registry.get()
try:
- _sessions[sess.hash_key] = sess
- _sessions[thread.get_ident()] = sess
+ session_registry.set(session)
return func()
finally:
if old is not None:
- _session[thread.get_ident()] = old
+ session_registry.set(old)
else:
- del _session[thread.get_ident()]
-
+ session_registry.clear()
Index: lib/sqlalchemy/util.py
===================================================================
--- lib/sqlalchemy/util.py (revision 942)
+++ lib/sqlalchemy/util.py (working copy)
@@ -377,12 +377,17 @@
self.defaultscope = scope
def __call__(self, scope=None):
+ """Get the object for the given scope, create a new one if none
exists"""
key = self._get_key(scope)
try:
return self.registry[key]
except KeyError:
return self.registry.setdefault(key, self.createfunc())
+ def get(self, scope=None):
+ """Get the object for the given scope, return None if none exists"""
+ return self.registry.get(self._get_key(scope))
+
def set(self, obj, scope=None):
self.registry[self._get_key(scope)] = obj