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