]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- renamed autoexpire to expire_on_commit
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 3 Aug 2008 18:03:57 +0000 (18:03 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 3 Aug 2008 18:03:57 +0000 (18:03 +0000)
- renamed SessionTransaction autoflush to reentrant_flush to more clearly state its purpose
- added _enable_transaction_accounting, flag for Mike Bernson which disables the whole 0.5 transaction state management; the system depends on expiry on rollback in order to function.

CHANGES
doc/build/content/session.txt
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/session.py
test/orm/dynamic.py
test/orm/session.py
test/orm/transaction.py

diff --git a/CHANGES b/CHANGES
index b70f7a877986f4960fc7bfa66f14db014be40039..efd5dfb4eb9041d926e4706670595b728a9f6067 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -12,6 +12,19 @@ CHANGES
     - The "entity_name" feature of SQLAlchemy mappers
       has been removed.  For rationale, see
       http://tinyurl.com/6nm2ne
+     
+    - the "autoexpire" flag on Session, sessionmaker(),
+      and scoped_session() has been renamed to 
+      "expire_on_commit".  It does not affect 
+      the expiration behavior of rollback().
+      
+    - a legacy-support flag 
+      "_enable_transation_accounting" flag added
+      to Session which when False, disables
+      all transaction-level object accounting,
+      including expire on rollback, expire 
+      on commit, new/deleted
+      list maintenance, and autoflush on begin.
       
     - The 'cascade' parameter to relation() accepts None
       as a value, which is equivalent to no cascades.
index b094a8fc79021dc8127aaadafb1977e5e55b58c8..6f7016deeb6616f1150eb656d9829b15379c3587 100644 (file)
@@ -258,7 +258,7 @@ The flush process *always* occurs within a transaction, even if the `Session` ha
 
 `commit()` is used to commit the current transaction.  It always issues `flush()` beforehand to flush any remaining state to the database; this is independent of the "autoflush" setting.   If no transaction is present, it raises an error.  Note that the default behavior of the `Session` is that a transaction is always present; this behavior can be disabled by setting `autocommit=True`.  In autocommit mode, a transaction can be initiated by calling the `begin()` method.
 
-Another behavior of `commit()` is that by default it expires the state of all instances present after the commit is complete.  This is so that when the instances are next accessed, either through attribute access or by them being present in a `Query` result set, they receive the most recent state.  To disable this behavior, configure `sessionmaker()` with `autoexpire=False`.
+Another behavior of `commit()` is that by default it expires the state of all instances present after the commit is complete.  This is so that when the instances are next accessed, either through attribute access or by them being present in a `Query` result set, they receive the most recent state.  To disable this behavior, configure `sessionmaker()` with `expire_on_commit=False`.
 
 Normally, instances loaded into the `Session` are never changed by subsequent queries; the assumption is that the current transaction is isolated so the state most recently loaded is correct as long as the transaction continues.  Setting `autocommit=True` works against this model to some degree since the `Session` behaves in exactly the same way with regard to attribute state, except no transaction is present.
 
@@ -270,7 +270,7 @@ Normally, instances loaded into the `Session` are never changed by subsequent qu
   a Connection, in which case the connection is still maintained (but still rolled back).
   * Objects which were initially in the *pending* state when they were added to the `Session` within the lifespan of the transaction are expunged, corresponding to their INSERT statement being rolled back.  The state of their attributes remains unchanged.
   * Objects which were marked as *deleted* within the lifespan of the transaction are promoted back to the *persistent* state, corresponding to their DELETE statement being rolled back.  Note that if those objects were first *pending* within the transaction, that operation takes precedence instead.
-  * All objects not expunged are fully expired.  This aspect of the behavior may be disabled by configuring `sessionmaker()` with `autoexpire=False`.
+  * All objects not expunged are fully expired.  
 
 With that state understood, the `Session` may safely continue usage after a rollback occurs (note that this is a new feature as of version 0.5).
 
index d28d367fe18b7a27ed7c2697cd849f8cb72bce54..447949283251332fc39eb2982b130f33ad3f140f 100644 (file)
@@ -126,9 +126,12 @@ def scoped_session(session_factory, scopefunc=None):
 def create_session(bind=None, **kwargs):
     """create a new [sqlalchemy.orm.session#Session].
 
-    The session by default does not begin a transaction, and requires that
-    flush() be called explicitly in order to persist results to the database.
-
+    The defaults of create_session() are the opposite of
+    that of sessionmaker(); autoflush and expire_on_commit
+    are false, autocommit is True. 
+    In this sense the session acts more like the "classic"
+    SQLAlchemy 0.3 session with these defaults.
+    
     It is recommended to use the [sqlalchemy.orm#sessionmaker()] function
     instead of create_session().
     """
@@ -143,7 +146,7 @@ def create_session(bind=None, **kwargs):
 
     kwargs.setdefault('autoflush', False)
     kwargs.setdefault('autocommit', True)
-    kwargs.setdefault('autoexpire', False)
+    kwargs.setdefault('expire_on_commit', False)
     return _Session(bind=bind, **kwargs)
 
 def relation(argument, secondary=None, **kwargs):
index 2d1f83dfe868991a97fee26d5beabc7d4cbda53f..6cf09049d4c60db12b6b181f832e29f27b31bdfa 100644 (file)
@@ -27,7 +27,7 @@ __all__ = ['Session', 'SessionTransaction', 'SessionExtension']
 
 
 def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False,
-                 autoexpire=True, **kwargs):
+                 expire_on_commit=True, **kwargs):
     """Generate a custom-configured [sqlalchemy.orm.session#Session] class.
 
     The returned object is a subclass of ``Session``, which, when instantiated
@@ -83,12 +83,18 @@ def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False,
       by any of these methods, the ``Session`` is ready for the next usage,
       which will again acquire and maintain a new connection/transaction.
 
-    autoexpire
-      When ``True``, all instances will be fully expired after each
-      ``rollback()`` and after each ``commit()``, so that all attribute/object
-      access subsequent to a completed transaction will load from the most
-      recent database state.
-
+    expire_on_commit
+      Defaults to ``True``. When ``True``, all instances will be fully expired after
+      each ``commit()``, so that all attribute/object access subsequent to a completed
+      transaction will load from the most recent database state.
+    
+    _enable_transaction_accounting
+      Defaults to ``True``.  A legacy-only flag which when ``False``
+      disables *all* 0.5-style object accounting on transaction boundaries,
+      including auto-expiry of instances on rollback and commit, maintenance of
+      the "new" and "deleted" lists upon rollback, and autoflush
+      of pending changes upon begin(), all of which are interdependent.
+    
     autoflush
       When ``True``, all query operations will issue a ``flush()`` call to
       this ``Session`` before proceeding. This is a convenience feature so
@@ -172,7 +178,7 @@ def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False,
     kwargs['bind'] = bind
     kwargs['autoflush'] = autoflush
     kwargs['autocommit'] = autocommit
-    kwargs['autoexpire'] = autoexpire
+    kwargs['expire_on_commit'] = expire_on_commit
 
     if class_ is None:
         class_ = Session
@@ -212,11 +218,11 @@ class SessionTransaction(object):
 
     """
 
-    def __init__(self, session, parent=None, autoflush=True, nested=False):
+    def __init__(self, session, parent=None, nested=False, reentrant_flush=False):
         self.session = session
         self._connections = {}
         self._parent = parent
-        self.autoflush = autoflush
+        self.reentrant_flush = reentrant_flush
         self.nested = nested
         self._active = True
         self._prepared = False
@@ -224,7 +230,9 @@ class SessionTransaction(object):
             raise sa_exc.InvalidRequestError(
                 "Can't start a SAVEPOINT transaction when no existing "
                 "transaction is in progress")
-        self._take_snapshot()
+
+        if self.session._enable_transaction_accounting:
+            self._take_snapshot()
 
     def is_active(self):
         return self.session is not None and self._active
@@ -250,10 +258,10 @@ class SessionTransaction(object):
         engine = self.session.get_bind(bindkey, **kwargs)
         return self._connection_for_bind(engine)
 
-    def _begin(self, autoflush=True, nested=False):
+    def _begin(self, reentrant_flush=False, nested=False):
         self._assert_is_active()
         return SessionTransaction(
-            self.session, self, autoflush=autoflush, nested=nested)
+            self.session, self, reentrant_flush=reentrant_flush, nested=nested)
 
     def _iterate_parents(self, upto=None):
         if self._parent is upto:
@@ -271,7 +279,7 @@ class SessionTransaction(object):
             self._deleted = self._parent._deleted
             return
 
-        if self.autoflush:
+        if not self.reentrant_flush:
             self.session.flush()
 
         self._new = weakref.WeakKeyDictionary()
@@ -288,13 +296,14 @@ class SessionTransaction(object):
         for s in set(self._new).union(self.session._new):
             self.session._expunge_state(s)
 
-        for s in self.session.identity_map.all_states():
-            _expire_state(s, None)
+        if self.session._enable_transaction_accounting:
+            for s in self.session.identity_map.all_states():
+                _expire_state(s, None)
 
     def _remove_snapshot(self):
         assert self._is_transaction_boundary
 
-        if not self.nested and self.session.autoexpire:
+        if not self.nested and self.session.expire_on_commit:
             for s in self.session.identity_map.all_states():
                 _expire_state(s, None)
 
@@ -348,7 +357,7 @@ class SessionTransaction(object):
             for subtransaction in stx._iterate_parents(upto=self):
                 subtransaction.commit()
 
-        if self.autoflush:
+        if not self.reentrant_flush:
             self.session.flush()
 
         if self._parent is None and self.session.twophase:
@@ -374,7 +383,8 @@ class SessionTransaction(object):
             if self.session.extension is not None:
                 self.session.extension.after_commit(self.session)
 
-            self._remove_snapshot()
+            if self.session._enable_transaction_accounting:
+                self._remove_snapshot()
 
         self.close()
         return self._parent
@@ -403,7 +413,8 @@ class SessionTransaction(object):
         for t in set(self._connections.values()):
             t[1].rollback()
 
-        self._restore_snapshot()
+        if self.session._enable_transaction_accounting:
+            self._restore_snapshot()
 
         if self.session.extension is not None:
             self.session.extension.after_rollback(self.session)
@@ -515,7 +526,8 @@ class Session(object):
         'merge', 'query', 'refresh', 'rollback', 'save',
         'save_or_update', 'scalar', 'update')
 
-    def __init__(self, bind=None, autoflush=True, autoexpire=True,
+    def __init__(self, bind=None, autoflush=True, expire_on_commit=True,
+                _enable_transaction_accounting=True,
                  autocommit=False, twophase=False, echo_uow=False,
                  weak_identity_map=True, binds=None, extension=None, query_cls=query.Query):
         """Construct a new Session.
@@ -539,7 +551,8 @@ class Session(object):
         self.hash_key = id(self)
         self.autoflush = autoflush
         self.autocommit = autocommit
-        self.autoexpire = autoexpire
+        self.expire_on_commit = expire_on_commit
+        self._enable_transaction_accounting = _enable_transaction_accounting
         self.twophase = twophase
         self.extension = extension
         self._query_cls = query_cls
@@ -558,7 +571,7 @@ class Session(object):
             self.begin()
         _sessions[self.hash_key] = self
 
-    def begin(self, subtransactions=False, nested=False, _autoflush=True):
+    def begin(self, subtransactions=False, nested=False, _reentrant_flush=False):
         """Begin a transaction on this Session.
 
         If this Session is already within a transaction, either a plain
@@ -584,14 +597,14 @@ class Session(object):
         if self.transaction is not None:
             if subtransactions or nested:
                 self.transaction = self.transaction._begin(
-                    nested=nested, autoflush=_autoflush)
+                    nested=nested, reentrant_flush=_reentrant_flush)
             else:
                 raise sa_exc.InvalidRequestError(
                     "A transaction is already begun.  Use subtransactions=True "
                     "to allow subtransactions.")
         else:
             self.transaction = SessionTransaction(
-                self, nested=nested, autoflush=_autoflush)
+                self, nested=nested, reentrant_flush=_reentrant_flush)
         return self.transaction  # needed for __enter__/__exit__ hook
 
     def begin_nested(self):
@@ -900,7 +913,7 @@ class Session(object):
         return self._query_cls(entities, self, **kwargs)
 
     def _autoflush(self):
-        if self.autoflush and (self.transaction is None or self.transaction.autoflush):
+        if self.autoflush and (self.transaction is None or not self.transaction.reentrant_flush):
             self.flush()
 
     def _finalize_loaded(self, states):
@@ -1035,12 +1048,12 @@ class Session(object):
 
         # remove from new last, might be the last strong ref
         if state in self._new:
-            if self.transaction:
+            if self._enable_transaction_accounting and self.transaction:
                 self.transaction._new[state] = True
             self._new.pop(state)
 
     def _remove_newly_deleted(self, state):
-        if self.transaction:
+        if self._enable_transaction_accounting and self.transaction:
             self.transaction._deleted[state] = True
 
         self.identity_map.discard(state)
@@ -1392,7 +1405,7 @@ class Session(object):
             return
 
         flush_context.transaction = transaction = self.begin(
-            subtransactions=True, _autoflush=False)
+            subtransactions=True, _reentrant_flush=True)
         try:
             flush_context.execute()
 
index 5d9b54b15cad8aa947c67036034163197cea9b27..3a36ccdd524470fdf6a2f799ec57c956bae6df7e 100644 (file)
@@ -160,7 +160,7 @@ class FlushTest(_fixtures.FixtureTest):
         mapper(User, users, properties={
             'addresses':dynamic_loader(mapper(Address, addresses))
         })
-        sess = create_session(autoexpire=False, autocommit=False, autoflush=True)
+        sess = create_session(expire_on_commit=False, autocommit=False, autoflush=True)
         u1 = User(name='jack')
         u1.addresses.append(Address(email_address='lala@hoho.com'))
         sess.add(u1)
index 7a7075a45b68f1a67ec6dbf7da2371d33aa250d1..09b9df05d898e590acb72ef398151d50be5688b3 100644 (file)
@@ -1057,7 +1057,7 @@ class DisposedStates(testing.ORMTest):
         self._test_session().expire_all()
     
     def test_rollback(self):
-        sess = self._test_session(autocommit=False, autoexpire=True)
+        sess = self._test_session(autocommit=False, expire_on_commit=True)
         sess.commit()
         
         sess.rollback()
index ce487a5f6ca7870c270298a8ca0176e6806aa729..24a2768c33f5e3ecf9a8b373f39981a621ca50a4 100644 (file)
@@ -181,7 +181,10 @@ class RollbackRecoverTest(TransactionTest):
         u1.name = 'edward'
         a1.email_address = 'foober'
         s.commit()
-        assert s.query(User).all() == [User(id=1, name='edward', addresses=[Address(email_address='foober')])]
+        self.assertEquals(
+            s.query(User).all(),
+            [User(id=1, name='edward', addresses=[Address(email_address='foober')])]
+        )
 
     @testing.requires.savepoints
     def test_pk_violation_with_savepoint(self):
@@ -351,7 +354,72 @@ class SavepointTest(TransactionTest):
         assert u1 not in s.deleted
 
 
+class AccountingFlagsTest(TransactionTest):
+    
+    def test_no_expire_on_commit(self):
+        sess = sessionmaker(expire_on_commit=False)()
+        u1 = User(name='ed')
+        sess.add(u1)
+        sess.commit()
+
+        testing.db.execute(users.update(users.c.name=='ed').values(name='edward'))
+        
+        assert u1.name == 'ed'
+        sess.expire_all()
+        assert u1.name == 'edward'
+
+    def test_rollback_no_accounting(self):
+        sess = sessionmaker(_enable_transaction_accounting=False)()
+        u1 = User(name='ed')
+        sess.add(u1)
+        sess.commit()
+
+        u1.name = 'edwardo'
+        sess.rollback()
+        
+        testing.db.execute(users.update(users.c.name=='ed').values(name='edward'))
+
+        assert u1.name == 'edwardo'
+        sess.expire_all()
+        assert u1.name == 'edward'
+
+    def test_commit_no_accounting(self):
+        sess = sessionmaker(_enable_transaction_accounting=False)()
+        u1 = User(name='ed')
+        sess.add(u1)
+        sess.commit()
 
+        u1.name = 'edwardo'
+        sess.rollback()
+
+        testing.db.execute(users.update(users.c.name=='ed').values(name='edward'))
+
+        assert u1.name == 'edwardo'
+        sess.commit()
+        
+        assert testing.db.execute(select([users.c.name])).fetchall() == [('edwardo',)]
+        assert u1.name == 'edwardo'
+
+        sess.delete(u1)
+        sess.commit()
+        
+    def test_preflush_no_accounting(self):
+        sess = sessionmaker(_enable_transaction_accounting=False, autocommit=True)()
+        u1 = User(name='ed')
+        sess.add(u1)
+        sess.flush()
+        
+        sess.begin()
+        u1.name = 'edwardo'
+        u2 = User(name="some other user")
+        sess.add(u2)
+        
+        sess.rollback()
+
+        sess.begin()
+        assert testing.db.execute(select([users.c.name])).fetchall() == [('ed',)]
+        
+    
 class AutocommitTest(TransactionTest):
     def test_begin_nested_requires_trans(self):
         sess = create_session(autocommit=True)