Transactions
If multiple users are accessing data at the same time, you might want to place your data table operations in a transaction. This ensures your Data Table operations are carried out as a group.
If multiple transactions are taking place at once, Anvil ensures each transaction’s Data Tables operations are completed together. From the Data Table’s perspective, it’s as if every transaction block is executing one after another.
If an exception is raised before the transaction is complete, all changes are discarded. If two transactions conflict - that is, they’ve written to the same row of a Data Table, or one transaction writes into a row another is reading from - one or both of them will be aborted.
Transactions are only available from server code (Server Modules and the Uplink).
Using a decorator
The easiest way to perform operations in a transaction is to call a function decorated with @anvil.tables.in_transaction. If an @anvil.tables.in_transaction function aborts due to conflict, it will try again (after a short timeout) up to 5 times before throwing an exception.
# Server code only
import anvil.tables
@anvil.server.callable
@anvil.tables.in_transaction
def do_update():
jane_smith = app_tables.people.get(Name="Jane Smith")
jane_smith['age'] = 21
app_tables.notes.add(Person=jane_smith,
Text="She's old enough to fly an aeroplane.")You can specify @anvil.tables.in_transaction after @anvil.server.callable to make a callable server function transactional. Make sure to specify @anvil.tables.in_transaction after @anvil.server.callable. Python applies function decorators from “inside” to “outside”, and you want the in_transaction decorator applied before you make the function callable.)
In a context manager
You can also create transactions by creating a with anvil.tables.Transaction(): block. This will not automatically retry conflicting transactions: If a transaction is aborted because of a conflict, a tables.TransactionConflict exception is thrown. You can also abort a transaction manually by calling .abort() on a transaction.
import anvil.tables
with anvil.tables.Transaction() as txn:
jane_smith = app_tables.people.get(Name="Jane Smith")
jane_smith['age'] += 10If you observe a TransactionConflict, it is generally safe to retry. You could write a loop that detects a conflict and tries again, but this is such a common pattern that it is usually preferable to use @anvil.tables.in_transaction (which retries the transaction automatically, up to 5 times, before throwing the TransactionConflict as normal.)
Nesting transactions
If you open a transaction inside another transaction, it will be “flattened” into the parent transaction: all its changes will commit or roll back together.
Requesting a relaxed transaction when nested within an ordinary (serialisable) transaction does nothing, as the combined transaction is already serialisable. Attempting to open an ordinary (serialisable) transaction within a relaxed transaction raises an error, as the already-open transaction cannot provide those guarantees.
Nested transactions can only be opened within the same server call as the parent transaction. For example, if you open a transaction in Uplink code and then make an anvil.server.call() to a server function while that transaction is open, and that server function attempts to start a transaction, it will raise an error.
Actions at the end of a transaction
Sometimes, you want to achieve side effects when a transaction completes. For example, you might want a model class that sends an email whenever certain fields are modified. You want this to occur once the transaction has succeeded, so that you do not receive incorrect emails about updates that failed or duplicate emails about transactions that had to be retried before they succeeded.
To do this, call anvil.tables.on_transaction_end(). This function takes three optional keyword parameters:
before_commit=: A function to call (with no arguments) if all code in this transaction has run without raising an exception, but before attempting to commit this transactionafter_commit=: A function to call (with no arguments) after this transaction has been committedafter_rollback=: A function to call (with no arguments) after this transaction has rolled back
If you call on_transaction_end() outside a transaction, the before_commit and after_commit functions will be called immediately. It is therefore safe to use on_transaction_end() to perform side effects that can happen immediately in non-transactional code but must be deferred to transaction end in transactional code.
on_transaction_end() can only be called within the same server call that started the transaction. For example, if you open a transaction in Uplink code and then make an anvil.server_call() to a server function while that transaction, and that server function attempts to call on_transaction_end(), it will raise an error.
Relaxed transactions
By default, Anvil provides fully serialisable transactions. These ensure that the database effects of your transactions are as if they occurred one after another, rather than concurrently. This makes it much easier to reason about, but the conservative bookkeeping required can result in false aborts, where a transaction is aborted even though it hasn’t conflicted with anything else.
If false aborts are causing performance issues in your application, you might be able to increase throughput with relaxed transactions, which omit the most conservative checks.
The difference between normal and relaxed transactions is that relaxed transactions do not detect conflicts with search queries. For example, the following code is not safe with relaxed transactions:
@tables.in_transaction
def get_or_create_row(name):
row = app_tables.people.get(name=name)
if row is None:
row = app_tables.people.add_row(name=name)
return rowThis is because, if a concurrent operation creates a row with the specified name in between the calls to get() and to add_row(), the relaxed transaction scheme will not detect it, and you will end up with two duplicate rows with the same value for the name column.
By contrast, conflicts between specific rows that are read or written during the transaction will be detected. The only conflicts that won’t be detected are changes that would have caused a row to match a search query.
REPEATABLE READ, whereas normal transactions are SERIALIZABLE.To use a relaxed transaction, add the argument relaxed=True to the in_transaction decorator or the Transaction() constructor:
@tables.in_transaction(relaxed=True)
def foo():
# This code will execute in a relaxed transaction
# (with automatic retries)with tables.Transaction(relaxed=True):
# This code will also execute in a relaxed transaction
# (without automatic retries)Do you still have questions?
Our Community Forum is full of helpful information and Anvil experts.