Skip to content

nested_rollback

Mike Bayer edited this page Nov 27, 2018 · 1 revision

Boiler plate

The following is just standard setup of an ORM session with a simple table mapped to the corresponding class.

Imports

Import the SQLAlchemy stuff we need:

>>> from sqlalchemy import create_engine, Column, Integer, String
>>> from sqlalchemy.orm import sessionmaker
>>> from sqlalchemy.ext.declarative import declarative_base

Declare mapping

Introduce a class Developer mapped to the table developer:

>>> Base = declarative_base()
>>> class Developer(Base):
...     __tablename__ = "developer"
...     id = Column(Integer, primary_key=True)
...     name = Column(String(60))
...     preferred_language = Column(String(30))

Setup database session

Create an engine pointing to an in-memory SQLite database and setup a session:

>>> engine = create_engine("sqlite:///")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(engine)
>>> session = Session()

Illustration of the unexpected behaviour

Now it gets interesting. The following shows the behaviour of a simple database operation when used in the toplevel transaction compared to a nested transaction.

Toplevel transaction

We create a new instance of the mapped class, add it to the session and make sure it actually hits the database:

>>> developer = Developer(name="Christopher", preferred_language="Python")
>>> session.add(developer)
>>> session.flush()

After rolling back the transaction, the instance is not attached to the session anymore and its attributes are still readable:

>>> session.rollback()
>>> developer in session
False
>>> developer.name, developer.preferred_language
('Christopher', 'Python')

I would expect the same behaviour the instance is added in a nested transaction and rolled back with the toplevel transaction.

Nested transaction

Let's try the same from a nested transaction:

>>> with session.begin_nested():
...     developer = Developer(name="Michael", preferred_language="Ruby")
...     session.add(developer)

Now the whole thing is undone by rolling back the outer transaction. When done this way, the instance is still attached to the session and accessing its attributes raises ObjectDeletedError:

>>> session.rollback()
>>> developer in session
True
>>> developer.name, developer.preferred_language     # doctest: +ELLIPSIS
Traceback (most recent call last):
   ...
ObjectDeletedError: Instance '<Developer at 0x...>' has been deleted, or its row is otherwise not present.

Expected behaviour

I would have expected that the following code snippets are 100% equivalent wrt. the final state of the ORM session under the condition that MODIFY_DATABASE completes successfully and that the session is initially clean:

Without nested transaction:

MODIFY_DATABASE(session)
session.flush()
session.rollback()

With nested transaction:

with session.begin_nested():
    MODIFY_DATABASE(session)
session.rollback()
Clone this wiki locally