Skip to content

Transaction

Transaction is a context manager that wraps one or more SQL operations in a single atomic unit: commits on success, rolls back on exception.

Nested with Transaction(model) blocks use PostgreSQL savepoints automatically — an exception in an inner block rolls back only that scope, leaving the outer transaction intact.

See Learn halfORM in half an hour for a full walkthrough.

Work that must wait for the COMMIT

A mutation only becomes a fact when the transaction commits. Anything whose effect leaves the database — a notification, a cache eviction, a websocket event — must therefore be deferred until then, or it announces rows that a rollback has taken back.

with Transaction(blog):
    post = Post(title='First post').ho_insert()
    Transaction(blog).after_commit(lambda: broadcast('post', 'create', post['id']))
    index(post)          # raises → nothing is broadcast
# the COMMIT having succeeded, the callback runs here

Transaction(model).after_commit(...) reaches the open transaction of the current thread from anywhere in the call stack: a model method can register a callback without knowing who opened the transaction, and without a reference to the object that did. Outside a transaction the callback runs immediately, a mutation committing on its own.

AsyncTransaction carries the same two methods as coroutines — await AsyncTransaction(model).after_commit(cb) — accepting coroutine functions and plain ones alike.


Transaction

Context manager for atomic database operations.

Wraps one or more SQL operations in a single transaction: commits on success, rolls back on exception. Transactions are per-thread and per-model instance.

Nested with Transaction(model) blocks use PostgreSQL savepoints automatically: an exception in an inner block rolls back only that inner block, leaving the outer transaction intact.

Parameters:

Name Type Description Default
model Model

the :class:~half_orm.model.Model instance whose connection should be used.

required
Example

Atomic insert of two related rows:

from half_orm.transaction import Transaction

with Transaction(blog):
    alice = Author(
        first_name='Alice', last_name='Martin',
        email='alice@example.com',
    ).ho_insert()
    Post(
        title='First post', content='Hello world',
        author_id=alice['id'],
    ).ho_insert()
# both rows are committed, or neither is

Nested transactions use savepoints:

with Transaction(blog):
    alice = Author(...).ho_insert()
    with Transaction(blog):          # savepoint
        Post(...).ho_insert()
        # exception here rolls back only the post, not Alice

Work that must wait for the COMMIT (see :meth:after_commit):

with Transaction(blog):
    post = Post(title='First post').ho_insert()
    Transaction(blog).after_commit(lambda: notify(post['id']))
# notify() runs here, the COMMIT having succeeded

New in version 0.18.0: nested Transaction blocks use savepoints.

New in version 1.2.0: :meth:after_commit and :meth:after_rollback.

Methods:

after_commit(callback)

Register callback to run once the transaction has committed.

A mutation only becomes a fact when the COMMIT succeeds: anything whose effect leaves the database — a notification, a cache eviction, a websocket event — has to wait for it, or it announces rows that a rollback has taken back.

Outside a transaction, a mutation commits on its own and the callback runs immediately. Inside one, it is queued and runs after the COMMIT of the outermost block, in registration order, with the connection back in autocommit mode. Rolling back a savepoint drops the callbacks registered inside it; releasing one hands them to the enclosing scope, whose own outcome then decides.

Registration is bound to the model and the current thread, not to this instance: Transaction(model).after_commit(...) reaches the open transaction from anywhere in the call stack, with no reference to the object that opened it.

A callback failure is reported on stderr, and the callbacks that follow still run. It is never raised to the caller: the COMMIT has already succeeded, and an exception here would describe the transaction as having failed.

Parameters:

Name Type Description Default
callback

a callable taking no argument. Its return value is ignored.

required
Example

Publish an event only if the whole unit of work lands:

@transaction
def publish(self, title):
    post = self.post_rfk(title=title).ho_insert()
    Transaction(self._ho_model).after_commit(
        lambda: broadcast('post', 'create', post['id']))
    self.index(post)       # may raise — nothing is broadcast

New in version 1.2.0.

after_rollback(callback)

Register callback to run if the transaction is rolled back.

The mirror of :meth:after_commit: the callback runs when the scope it was registered in is undone — the rollback of the outermost block, the rollback of the savepoint it belongs to, or a COMMIT that fails. Releasing a savepoint hands its callbacks to the enclosing scope, which may still roll back.

Outside a transaction there is nothing to undo and the callback is dropped — the symmetric case of :meth:after_commit running it at once.

As with :meth:after_commit, a callback failure is reported on stderr and never raised: here it would displace the exception the caller is already handling.

Parameters:

Name Type Description Default
callback

a callable taking no argument. Its return value is ignored.

required

New in version 1.2.0.