From: Mike Bayer Date: Sun, 3 Aug 2008 18:03:57 +0000 (+0000) Subject: - renamed autoexpire to expire_on_commit X-Git-Tag: rel_0_5beta3~7 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4769ea895b3312e2d27d52b45816771df8a5c5c7;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - renamed autoexpire to expire_on_commit - 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. --- diff --git a/CHANGES b/CHANGES index b70f7a8779..efd5dfb4eb 100644 --- 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. diff --git a/doc/build/content/session.txt b/doc/build/content/session.txt index b094a8fc79..6f7016deeb 100644 --- a/doc/build/content/session.txt +++ b/doc/build/content/session.txt @@ -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). diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index d28d367fe1..4479492832 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -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): diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 2d1f83dfe8..6cf09049d4 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -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() diff --git a/test/orm/dynamic.py b/test/orm/dynamic.py index 5d9b54b15c..3a36ccdd52 100644 --- a/test/orm/dynamic.py +++ b/test/orm/dynamic.py @@ -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) diff --git a/test/orm/session.py b/test/orm/session.py index 7a7075a45b..09b9df05d8 100644 --- a/test/orm/session.py +++ b/test/orm/session.py @@ -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() diff --git a/test/orm/transaction.py b/test/orm/transaction.py index ce487a5f6c..24a2768c33 100644 --- a/test/orm/transaction.py +++ b/test/orm/transaction.py @@ -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)