Michael Bayer wrote:
dmiller wrote:
s1 = objectstore.get_session()
s2 = s1.begin()
# do stuff
s1.commit() # ERROR: uncommitted nested sessions
s2.commit() # not a real commit
s1.commit() # commit all changes including those made on s2
# do more stuff (all changes are now registered with s1)
s2.commit() # ERROR: session expired - parent session has been committed
s2 = s1.begin()
# do stuff
s2.commit() # ahh, that's better
s1.commit() # yeah, now you're getting it

Does this require two "commit" calls for every "begin" ?  thats a little
strange.

No, but it takes less begins since the outer session does not need to be "begun". Sorry 
for the confusion. However, while making a patch I realized that I needed a way to "end" 
the nested environment because we can't use new session (objects would be associated with the wrong 
session after the nested session was discarded). Here's an updated example:

def foo(s):
   # do stuff
   s.commit() # treat it like a normal session
   # do more stuff
   s.commit()

def bar(s):
   # do other stuff
   s.commit()
   # make some changes that will not be committed

s = objectstore.get_session()
# do stuff - will not be committed by foo() or bar()
s.begin() foo(s)
bar(s)
s.end()
s.commit() # commit stuff from before begin

Notice what's happening here:

1. foo() and bar can commit as many times as necessary (without a corresponding 
begin). This is the same as if the function had been written with no intention 
of being executed in a nested begin/commit environment. With the current model, 
foo() would have to be rewritten to work in a nested environment (it would need 
an extra begin before each commit, which is currently legal but not required 
when operating in a non-nested environment).

2. A begin() is only needed before a session is passed into a nested 
environment. This causes commits in the nested environment to only write 
changes that are made within the nested environment.

3. An end() is needed for every begin()

4. This model has the added advantage of not requiring the developer to balance 
begin/commit calls within the nested call. A begin() starts a nested session. A 
commit on that nested session commits changes since that session began, but it 
does not end the nested session. Multiple commits are legal after begin just 
like in non-nested environments.

 also it doesnt really prevent the error from happening, does it
?  they still can say:

def func(s):
     s2 = s1.begin()
     # do stuff
     s2.commit() # save changes up to here
     # do more stuff
     s1.commit() # save the remaining changes

which still does the s1.commit() that is supposed to happen outside of
func() ?  are we just looking for things that make it more visually
obvious where the begins/commits occur ?

That is incorrect code. func() should be written as if it was getting an normal 
(not nested) session. It does not need know that it is being executed in a 
nested environment:

def func(s):
   # do stuff
   s.commit()
   # do more stuff
   s.commit()

The caller decides if func will be called in a nested scope. It could be called 
this way (nested):

s = objectstore.get_session()
# do stuff - will not be committed by func
s.begin()
func(s)
s.end()
s.commit() # commit stuff done before calling func

...or this way (not nested):

s = objectstore.get_session()
func(s)



how about "begin()/commit()" for the two-phase version and "commit_all()"
for the one-phase, so that theyre just totally separate ?


I have implemented something like this (patch attached). The problem with 
commit/commit_all is that code being written in a nested environment is 
different than that being written in the outer environment (nested environments 
would never call commit_all, correct?). The parent environment should be the 
one calling begin/end while the child environment just uses the session like a 
normal, non-nested session. That way any block of code that uses a session can 
be nested by any other block of code that has a session.

I also changed Session.rollback slightly. It no longer depends on the begin/end 
cycle (same as commit), and it now has the ability to rollback instances (also 
like commit).

My only other suggestion is to change begin/end to begin_nested/end_nested, but 
that seems too verbose so I didn't bother in my patch.

~ Daniel
Index: lib/sqlalchemy/mapping/objectstore.py
===================================================================
--- lib/sqlalchemy/mapping/objectstore.py       (revision 972)
+++ lib/sqlalchemy/mapping/objectstore.py       (working copy)
@@ -25,6 +25,9 @@
 # printed to standard output.  also can be affected by creating an engine
 # with the "echo_uow=True" keyword argument.
 LOG = False
+
+class SessionError(Exception):
+    pass
     
 class Session(object):
     """Maintains a UnitOfWork instance, including transaction state."""
@@ -39,8 +42,7 @@
         defaults to the id of the Session instance.
         """
         self.uow = UnitOfWork()
-        self.parent_uow = None
-        self.begin_count = 0
+        self.uow_stack = []
         self.nest_transactions = nest_transactions
         if hash_key is None:
             self.hash_key = id(self)
@@ -86,41 +88,38 @@
     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.begin_count += 1
-        if self.parent_uow is not None:
-            return
-        self.parent_uow = self.uow            
+        self.uow_stack.append(self.uow)
         self.uow = UnitOfWork(identity_map = self.uow.identity_map)
-        
+    
+    def end(self):
+        """ends a UnitOfWork transaction started by begin.  all pending 
changes on the
+        current UnitOfWork are discarded and the UnitOfWork that was in place 
before
+        begin() was called is resumed.  raises an exception if there was no 
corresponding
+        begin"""
+        try:
+            self.uow = self.uow_stack.pop()
+        except IndexError:
+            raise SessionError("Session.end before Session.begin")
+    
     def commit(self, *objects):
         """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 individual objects are submitted, then only those objects are 
committed, and the 
-        begin/commit cycle is not affected."""
-        # if an object list is given, commit just those but dont
-        # change begin/commit status
-        if len(objects):
-            self.uow.commit(*objects)
-            return
-        if self.parent_uow is not None:
-            self.begin_count -= 1
-            if self.begin_count > 0:
-                return
-        self.uow.commit()
-        if self.parent_uow is not None:
-            self.uow = self.parent_uow
-            self.parent_uow = None
+        if individual objects are submitted, then only those objects are 
committed"""
+        self.uow.commit(*objects)
 
-    def rollback(self):
-        """rolls back the current UnitOfWork transaction, in the case that 
begin()
-        has been called.  The changes logged since the begin() call are 
discarded."""
-        if self.parent_uow is None:
-            raise "UOW transaction is not begun"
-        self.uow = self.parent_uow
-        self.parent_uow = None
-        self.begin_count = 0
+    def rollback(self, *objects):
+        """rolls back the current UnitOfWork transaction.  all pending changes 
logged since
+        the session started or since begin() was called (whichever was most 
recent) are
+        discarded. objects with changes should either be discarded or placed 
back in a
+        session with import_instance
+        if individual objects are submitted, then only attributes on those 
objects are
+        rolled back"""
+        if objects:
+            self.uow.rollback(*objects)
+        else:
+            self.uow = UnitOfWork(identity_map = self.uow.identity_map)
         
     def register_clean(self, obj):
         self._bind_to(obj)
@@ -411,9 +410,10 @@
         commit_context.post_exec()
         
 
-    def rollback_object(self, obj):
-        """'rolls back' the attributes that have been changed on an object 
instance."""
-        self.attributes.rollback(obj)
+    def rollback_object(self, *objects):
+        """'rolls back' the attributes that have been changed on the given 
object instances."""
+        for obj in objects:
+            self.attributes.rollback(obj)
             
 class UOWTransaction(object):
     """handles the details of organizing and executing transaction tasks 
Index: test/objectstore.py
===================================================================
--- test/objectstore.py (revision 972)
+++ test/objectstore.py (working copy)
@@ -91,26 +91,48 @@
         class User(object):pass
         m = mapper(User, users)
         def name_of(id):
-            return users.select(users.c.user_id == 
id).execute().fetchone().user_name
-        name1 = "Oliver Twist"
+            try:
+                return users.select(users.c.user_id == 
id).execute().fetchone().user_name
+            except:
+                return None
+        name1 = 'Oliver Twist'
         name2 = 'Mr. Bumble'
+        name3 = 'The Thief'
         self.assert_(name_of(7) != name1, msg="user_name should not be %s" % 
name1)
         self.assert_(name_of(8) != name2, msg="user_name should not be %s" % 
name2)
+        self.assert_(name_of(9) != name3, msg="user_name should not be %s" % 
name3)
         s = objectstore.get_session()
-        s.begin()
-        s.begin()
         m.get(7).user_name = name1
-        s.begin()
+        s.begin() # begin nested
         m.get(8).user_name = name2
+        s.begin() # begin second nested
+        m.get(9).user_name = name3
         s.commit()
+        s.commit() # extra commit should have no affect
+        u = User() # create a user that will not be committed
+        u.user_id = 10
+        u.user_name = 'not persistent'
+        s.end()
         self.assert_(name_of(7) != name1, msg="user_name should not be %s" % 
name1)
         self.assert_(name_of(8) != name2, msg="user_name should not be %s" % 
name2)
+        self.assert_(name_of(9) == name3, msg="user_name should be %s" % name3)
+        self.assert_(name_of(10) == None, msg="user with user_id=10 should not 
exist")
         s.commit()
+        s.end()
         self.assert_(name_of(7) != name1, msg="user_name should not be %s" % 
name1)
-        self.assert_(name_of(8) != name2, msg="user_name should not be %s" % 
name2)
+        self.assert_(name_of(8) == name2, msg="user_name should be %s" % name2)
+        self.assert_(name_of(9) == name3, msg="user_name should be %s" % name3)
+        self.assert_(name_of(10) == None, msg="user with user_id=10 should not 
exist")
         s.commit()
         self.assert_(name_of(7) == name1, msg="user_name should be %s" % name1)
         self.assert_(name_of(8) == name2, msg="user_name should be %s" % name2)
+        self.assert_(name_of(9) == name3, msg="user_name should be %s" % name3)
+        self.assert_(name_of(10) == None, msg="user with user_id=10 should not 
exist")
+        try:
+            s.end()
+            self.assert_(False, msg="s.end without s.begin should raise an 
exception")
+        except objectstore.SessionError:
+            pass
 
 
 class PKTest(AssertMixin):

Reply via email to