Commits

Mike Bayer committed 48b6d8b

added new style of begin/commit which returns a tranactional object

  • Participants
  • Parent commits a04c6e9

Comments (0)

Files changed (5)

doc/build/content/document_base.myt

     onepage='documentation'
     index='index'
     title='SQLAlchemy Documentation'
-    version = '0.1.0'
+    version = '0.1.1'
 </%attr>
 
 <%method title>

doc/build/content/unitofwork.myt

     <&|doclib.myt:item, name="begin", description="Controlling Scope with begin()" &>
     
     <p>The "scope" of the unit of work commit can be controlled further by issuing a begin().  A begin operation constructs a new UnitOfWork object and sets it as the currently used UOW.  It maintains a reference to the original UnitOfWork as its "parent", and shares the same "identity map" of objects that have been loaded from the database within the scope of the parent UnitOfWork.  However, the "new", "dirty", and "deleted" lists are empty.  This has the effect that only changes that take place after the begin() operation get logged to the current UnitOfWork, and therefore those are the only changes that get commit()ted.  When the commit is complete, the "begun" UnitOfWork removes itself and places the parent UnitOfWork as the current one again.</p>
+<p>The begin() method returns a transactional object, upon which you can call commit() or rollback().  <b>Only this transactional object controls the transaction</b> - commit() upon the Session will do nothing until commit() or rollback() is called upon the transactional object.</p>
     <&|formatting.myt:code&>
         # modify an object
         myobj1.foo = "something new"
         
         # begin an objectstore scope
         # this is equivalent to objectstore.get_session().begin()
-        objectstore.begin()
+        trans = objectstore.begin()
         
         # modify another object
         myobj2.lala = "something new"
         
         # only 'myobj2' is saved
-        objectstore.commit()
+        trans.commit()
     </&>
-    <p>As always, the actual database transaction begin/commit occurs entirely within the objectstore.commit() operation.</p>
-    <p>At the moment, begin/commit supports the same "nesting" behavior as the SQLEngine (note this behavior is not the original "nested" behavior), meaning that repeated calls to begin() will increment a counter, which is not released until that same number of commit() statements occur.</p>
+    <p>begin/commit supports the same "nesting" behavior as the SQLEngine (note this behavior is not the original "nested" behavior), meaning that many begin() calls can be made, but only the outermost transactional object will actually perform a commit().  Similarly, calls to the commit() method on the Session, which might occur in function calls within the transaction, will not do anything; this allows an external function caller to control the scope of transactions used within the functions.</p>
     </&>
     <&|doclib.myt:item, name="transactionnesting", description="Nesting UnitOfWork in a Database Transaction" &>
     <p>The UOW commit operation places its INSERT/UPDATE/DELETE operations within the scope of a database transaction controlled by a SQLEngine:

lib/sqlalchemy/mapping/objectstore.py

         """
         return (class_, table.hash_key(), tuple([row[column] for column in primary_key]))
     get_row_key = staticmethod(get_row_key)
-    
+
+    class UOWTrans(object):
+        def __init__(self, parent, uow, isactive):
+            self.__parent = parent
+            self.__isactive = isactive
+            self.__uow = uow
+        isactive = property(lambda s:s.__isactive)
+        parent = property(lambda s:s.__parent)
+        uow = property(lambda s:s.__uow)
+        def begin(self):
+            return self.parent.begin()
+        def commit(self):
+            self.__parent._trans_commit(self)
+            self.__isactive = False
+        def rollback(self):
+            self.__parent._trans_rollback(self)
+            self.__isactive = False
+
     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            
+            return Session.UOWTrans(self, self.uow, False)
+        self.parent_uow = self.uow
         self.uow = UnitOfWork(identity_map = self.uow.identity_map)
-        
+        return Session.UOWTrans(self, self.uow, True)
+    
+    def _trans_commit(self, trans):
+        if trans.uow is self.uow and trans.isactive:
+            self.uow.commit()
+            self.uow = self.parent_uow
+            self.parent_uow = None
+    def _trans_rollback(self, trans):
+        if trans.uow is self.uow:
+            self.uow = self.parent_uow
+            self.parent_uow = None
+                        
     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
         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 self.parent_uow is None:
+            self.uow.commit()
 
     def rollback(self):
         """rolls back the current UnitOfWork transaction, in the case that begin()

test/objectstore.py

     def setUpAll(self):
         db.echo = False
         users.create()
-        tables.user_data()
         db.echo = testbase.echo
     def tearDownAll(self):
         db.echo = False
     def setUp(self):
         objectstore.get_session().clear()
         clear_mappers()
+        tables.user_data()
+        #db.echo = "debug"
+    def tearDown(self):
+        tables.delete_user_data()
         
     def test_nested_begin_commit(self):
         """test nested session.begin/commit"""
         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)
         s = objectstore.get_session()
-        s.begin()
-        s.begin()
+        trans = s.begin()
+        trans2 = s.begin()
         m.get(7).user_name = name1
-        s.begin()
+        trans3 = s.begin()
         m.get(8).user_name = name2
-        s.commit()
+        trans3.commit()
+        s.commit() # should do nothing
         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)
-        s.commit()
+        trans2.commit()
+        s.commit()  # should do nothing
         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)
-        s.commit()
+        trans.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)
 
+    def test_nested_rollback(self):
+        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"
+        name2 = 'Mr. Bumble'
+        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)
+        s = objectstore.get_session()
+        trans = s.begin()
+        trans2 = s.begin()
+        m.get(7).user_name = name1
+        trans3 = s.begin()
+        m.get(8).user_name = name2
+        trans3.rollback()
+        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)
+        trans2.commit()
+        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)
+        trans.commit()
+        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)
 
 class PKTest(AssertMixin):
     def setUpAll(self):
         dict(user_id = 8, user_name = 'ed'),
         dict(user_id = 9, user_name = 'fred')
     )
-    
+def delete_user_data():
+    users.delete().execute()
+    db.commit()
+        
 def data():
     delete()