Well, I was pursuing this differently. While I am moving in the same direction where UnitOfWork is not really dealt with publically, I have the Session's main purpose as managing the "scope" of a set of unit of works, such as thread-local, per-application, or custom. that way someone can define some other algorithm for managing the scope of unitofwork, and install it as the "global" algorithm, or also "push" and "pop" the Session object into the current scope so that their algorithm can take effect just for certain blocks of code....such as, you might define a function that returns an ID for the currently focused window in your application. I have put up preliminary docs for this on the site so you can see what I was getting at....but its not a final decision or anything like that, its just further along than this particular patch. it is more complicated but i think its a valuable function.

I do like the idea of removing "session" from the UnitOfWork and moving the "bind_to" operation totally within the Session, that way UnitOfWork doesnt need the parent "session". But if the Session is really going to manage the begin/commit thing, then the "parent" idea should come out of UnitOfWork too....UOW should just have "commit" which does the actual work, and thats it. perhaps Session could just set or otherwise associate attributes with the UnitOfWork so that UOW doesnt need to be aware of it. also, what do we gain by providing a "create a UnitOfWork" function ? I have that function too....but is there really some other way of constructing a single UnitOfWork someone might need ?

also, this patch removes the ability to "nest" begins/commits, having only the "outermost" begin/commit take effect, which is something people specifically asked for. they want to say:

        def foo():
                objectstore.begin()
                .. do stuff
                objectstore.commit()

and then have foo() callable inside a larger commit:

        objectstore.begin()
        foo()
        bar()
        objectstore.commit()

and have foo()'s transaction pulled upwards to the outermost transaction. you need a begin/commit counter to do that.

anyway, I committed my most recent changes just now, so lets work from there...check out the docs to see how i was describing it. ive no issue with using get_session() instead of session(), and factoring out some of begin/commit into the session will be good if it simplifies UnitOfWork.

- mike

On Feb 12, 2006, at 3:02 PM, Daniel Miller wrote:

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




-------------------------------------------------------
This SF.net email is sponsored by: Splunk Inc. Do you grep through log files
for problems?  Stop!  Download the new AJAX search engine that makes
searching your log files as easy as surfing the  web.  DOWNLOAD SPLUNK!
http://sel.as-us.falkag.net/sel?cmd=lnk&kid=103432&bid=230486&dat=121642
_______________________________________________
Sqlalchemy-users mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/sqlalchemy-users

Reply via email to