Chapter 11 · Database backends
11.5 Transactions: autocommit, atomic and savepoints
Django runs a connection in autocommit, each statement a transaction of its own, until code asks for more with atomic, and the code it wraps is an atomic block. Atomic.__enter__ and Atomic.__exit__ turn atomic blocks nested one inside another into a single transaction, with a savepoint, a named point in the transaction that can be rolled back to without undoing what came before it, for each inner block that asks for one. They keep their state on the connection, where BaseDatabaseWrapper marks a transaction that must be rolled back.
A database makes its changes in transactions, which become visible to others all at once when they commit and are undone when they roll back. PEP 249, which Django’s drivers follow, has a driver open its connection with autocommit off, so that statements gather in a transaction until the program commits; BaseDatabaseWrapper turns autocommit on, unless the alias’s "AUTOCOMMIT" is False. Django’s documentation gives the reason: the SQL standard has each statement begin a transaction that must be committed or rolled back explicitly, which is not always convenient, and most databases offer autocommit to spare it.
Code that needs several statements to stand or fall together wraps them in atomic, as a with statement or a decorator. The outermost block, the one no other open block contains, begins a transaction as it is entered where autocommit is on, and as it is left commits it, or rolls it back where an exception is leaving or the transaction is marked broken. A block entered inside another makes a savepoint, unless it was made with savepoint=False or the transaction is already marked broken, and as it is left releases the savepoint, or rolls back to it and then releases it, so that a failure in an inner block undoes that block’s work alone.
savepoint=False, and the ways out of each, as recorded on SQLite. Other backends send other things: Django sends no "BEGIN" of its own there but turns the driver’s autocommit off, and on Oracle a release sends nothing.The state is the connection’s, not the Atomic object’s. The class’s docstring says why: the same instance can be entered again, as a decorator or in several with statements, even while it is open, and since each thread has connections of its own this is safe across threads. What on_commit keeps beside it is Work held until the commit: on_commit.
Autocommit: a statement is its own transaction
"AUTOCOMMIT" is True unless an alias says otherwise (Connections by alias: DATABASES and ConnectionHandler), and BaseDatabaseWrapper hands it to BaseDatabaseWrapper as soon as the driver’s connection is open (Opening and closing a connection). set_autocommit refuses inside an atomic block, which Django’s documentation says would break atomicity: BaseDatabaseWrapper raises TransactionManagementError. Turning autocommit on, it calls the backend’s BaseDatabaseWrapper with True; turning it off, it calls it with False, inside debug_transaction, which logs a "BEGIN" where queries are logged (Cursors: executing a statement, the query log and execute wrappers); and given force_begin_transaction_with_broken_autocommit=True, which only Atomic.__enter__ passes, it begins a transaction with the backend’s own method instead, where the backend has one, as SQLite’s does, and leaves the driver’s autocommit as it was. It records the value it was given in BaseDatabaseWrapper, which BaseDatabaseWrapper returns, and where it turned autocommit on after a commit, it runs the callbacks of on_commit. Both set_autocommit and get_autocommit open the driver’s connection first if it is not open.
The recordings on this page ran on SQLite: a line at the margin is Python as it was run, with -> before what it returned, the lines set in under it are the calls it set going, a line SQL is a statement as Django handed it to its cursor, and a line that begins sqlite is the statement as SQLite ran it. In the plain case:
connection.get_autocommit() -> True
connection.autocommit -> True
connection.connection.isolation_level -> None
Port.objects.create(name='Cork', country='Ireland')
mark_for_rollback_on_error
SQL INSERT INTO "fleet_port" ("name", "country") VALUES (%s, %s) RETURNING "fleet_port"."id" with params ('Cork', 'Ireland')
sqlite INSERT INTO "fleet_port" ("name", "country") VALUES ('Cork', 'Ireland') RETURNING "fleet_port"."id"
mark_for_rollback_on_error resumes
SQLite’s driver is in its own autocommit when its connection’s isolation_level is None. The create sent one "INSERT" and nothing began or committed it: SQLite committed the statement as it ended. A model whose parent has a table of its own is saved table by table, and Model.save_base wraps that save in atomic(savepoint=False). Where a failure is to be caught and recovered from, as in QuerySet, the ORM uses a plain atomic (Creating, updating and deleting through a queryset).
Turning autocommit off and on, backend by backend
BaseDatabaseWrapper raises NotImplementedError, and each backend’s throws the driver’s own switch. PostgreSQL’s and Oracle’s set the autocommit attribute of the driver’s connection, and MySQL’s calls the driver connection’s autocommit method. On these three, turning autocommit off is the whole of beginning a transaction: Django sends no statement, and the transaction begins with the next one.
SQLite’s DatabaseWrapper sets the driver connection’s isolation_level, to None for autocommit and to the empty string, the driver’s own default, for none. An atomic block calls it only as an outermost block ends, to turn autocommit on again. SQLite’s backend is the one of the four with DatabaseWrapper, which sends "BEGIN" through Django’s cursor, with the mode that "OPTIONS" names as "transaction_mode" where it names one (SQLite), and leaves the driver in its autocommit. Its docstring gives the reason: staying in autocommit works around a bug of the driver that breaks savepoints when autocommit is disabled, the bug being, in the comment on SQLite’s DatabaseWrapper, that the driver commits before each savepoint unless isolation_level is None. So on SQLite a block’s "BEGIN" is Django’s statement, an SQL line in the recordings, and its commit is the driver’s.
BaseDatabaseWrapper and BaseDatabaseWrapper refuse inside a block as set_autocommit does, and call the backend’s BaseDatabaseWrapper or BaseDatabaseWrapper, which call the driver connection’s own. commit also sets BaseDatabaseWrapper; rollback sets BaseDatabaseWrapper back to False and empties BaseDatabaseWrapper.
What the connection keeps
BaseDatabaseWrapper sets the state on each connection, autocommit starting as False, PEP 249’s default.
BaseDatabaseWrapper.connect empties the two stacks and sets in_atomic_block, needs_rollback and closed_in_transaction to False; commit_on_exit, rollback_exc and savepoint_state it leaves.| attribute | what it holds |
|---|---|
Base |
whether autocommit is on, as set_autocommit last left it |
Base |
whether an atomic block is open, or an outermost block pretends one is |
Base |
a stack with an entry for each block entered inside a transaction: its savepoint’s name, or None |
Base |
a stack of the open Atomic objects |
Base |
whether the outermost block commits as it ends: False where autocommit was off when it was entered |
Base |
that the transaction is broken, and no statement may be sent until a rollback |
Base |
the exception mark_ last stored, inside an atomic block, given as the cause when a statement is refused |
Base |
the counter that numbers savepoints |
Base |
that the connection was closed inside a block |
The functions of django.db.transaction
Apart from atomic, mark_for_rollback_on_error and non_atomic_requests, each told in a section of its own, the functions of django each take an alias, using=None meaning "default", find the connection with get_connection, and call the connection’s method of the same name; savepoint_create calls BaseDatabaseWrapper, and the deprecated savepoint calls savepoint_create. The checks are all the connection’s; the functions add none.
Entering a block: Atomic.__enter__
atomic(using=None, savepoint=True, durable=False) makes an Atomic of its three arguments; called with a function in place of an alias, as the bare decorator @atomic is, it wraps the function in an Atomic for "default". Atomic is a contextlib, so as a decorator it enters itself around each call.
Atomic.__enter__ goes in four steps:
- A durable block is refused where another is open, unless the innermost open block is one that
TestCaseopened. - Where no block is open, this block is the outermost: it sets
BaseDatabaseWrapperto.commit_on_exit TrueandBaseDatabaseWrapperto.needs_rollback False. If autocommit is already off, it also setsBaseDatabaseWrapperand sets.in_atomic_block commit_on_exittoFalse: it pretends to be inside a block already, so as to bypass the code that turns autocommit off. - Inside an open block, real or pretended, it asks for a savepoint and pushes the name onto
BaseDatabaseWrapper, or pushes.savepoint_ids Nonewhere it was made withsavepoint=Falseor the transaction is already marked broken, which avoids a useless savepoint and keepsneeds_rollbackfrom being overwritten before the rollback is done. With no block open, it begins the transaction throughBaseDatabaseWrapperand sets.set_autocommit in_atomic_block. - It pushes itself onto
BaseDatabaseWrapper..atomic_blocks
Two blocks entered as a with statement enters them:
outer = transaction.atomic()
inner = transaction.atomic()
the connection holds: in_atomic_block=False, savepoint_ids=[], atomic_blocks=[], commit_on_exit=True, needs_rollback=False
outer.__enter__()
Atomic.__enter__
BaseDatabaseWrapper.set_autocommit(autocommit=False, force_begin_transaction_with_broken_autocommit=True)
DatabaseWrapper._start_transaction_under_autocommit
SQL BEGIN
sqlite BEGIN
the connection holds: in_atomic_block=True, savepoint_ids=[], atomic_blocks=[an Atomic], commit_on_exit=True, needs_rollback=False
...
inner.__enter__()
Atomic.__enter__
BaseDatabaseWrapper.savepoint
SQL SAVEPOINT "s(the thread)_x1"
sqlite SAVEPOINT "s(the thread)_x1"
-> 's(the thread)_x1'
the connection holds: in_atomic_block=True, savepoint_ids=['s(the thread)_x1'], atomic_blocks=[an Atomic, an Atomic], commit_on_exit=True, needs_rollback=False
Savepoints
BaseDatabaseWrapper makes a savepoint and returns its name, or returns None and sends nothing where BaseDatabaseWrapper says no. The name is "s", the thread’s number without its sign, "_x", and the connection’s counter BaseDatabaseWrapper, which only BaseDatabaseWrapper sets back to 0. The statement is written by the connection’s operations object (BaseDatabaseWrapper): BaseDatabaseOperations writes "SAVEPOINT" and the name quoted by quote_name, and its siblings BaseDatabaseOperations and BaseDatabaseOperations write "RELEASE SAVEPOINT" and "ROLLBACK TO SAVEPOINT"; none of the four backends overrides them. BaseDatabaseWrapper and BaseDatabaseWrapper send the other two behind the same check, and savepoint_rollback also drops the callbacks of on_commit registered while the savepoint was on BaseDatabaseWrapper, where one made with savepoint_create of django never is (Work held until the commit: on_commit).
The check is the flag BaseDatabaseFeatures, one of the connection’s features, and autocommit off, a comment saying savepoints cannot be made outside a transaction. SQLite’s asks for BaseDatabaseWrapper instead, its comment saying that with autocommit on savepoints make no sense, except inside atomic blocks. So on SQLite savepoint_create outside a block returns None even with autocommit turned off by hand, where the other backends make the savepoint:
transaction.set_autocommit(False)
...
connection.in_atomic_block -> False
sid = transaction.savepoint_create()
BaseDatabaseWrapper.savepoint
DatabaseWrapper._savepoint_allowed -> False
-> None
uses_savepoints is True on all four backends (What a database can do: BaseDatabaseFeatures). Oracle’s backend never sends "RELEASE SAVEPOINT": its DatabaseWrapper sends nothing and, where queries are logged, logs a line marked as faked, the comment saying that Oracle cannot release savepoints and that the line keeps the count of queries the same as on the other backends.
Leaving a block: Atomic.__exit__
Atomic.__exit__ first takes the block off the stacks: it pops BaseDatabaseWrapper, and pops the block’s entry from BaseDatabaseWrapper where that stack has one. Where that stack is empty, the block is an outermost block that turned autocommit off, and BaseDatabaseWrapper is set to False at once; the comment says this is to allow BaseDatabaseWrapper and BaseDatabaseWrapper, which refuse inside a block. Then it takes one of three branches, and a finally finishes an outermost block. The first is for a connection closed inside a block, whose transaction the database rolls back by itself: each exit does nothing more, and the outermost’s finally sets the driver’s connection to None (Closed inside an atomic block). __exit__ returns nothing, so an exception that was leaving the block goes on leaving it: atomic undoes work for an exception and never swallows one.
With no exception and BaseDatabaseWrapper unset, an inner block releases its savepoint, where it made one, and an outermost block that turned autocommit off commits. The finally then turns autocommit on again, calling BaseDatabaseWrapper with True, which runs the callbacks of on_commit:
inner.__exit__(None, None, None)
Atomic.__exit__
BaseDatabaseWrapper.savepoint_commit('s(the thread)_x1')
SQL RELEASE SAVEPOINT "s(the thread)_x1"
sqlite RELEASE SAVEPOINT "s(the thread)_x1"
the connection holds: in_atomic_block=True, savepoint_ids=[], atomic_blocks=[an Atomic], commit_on_exit=True, needs_rollback=False
outer.__exit__(None, None, None)
Atomic.__exit__
BaseDatabaseWrapper.commit
BaseDatabaseWrapper._commit
sqlite3.Connection.commit
sqlite COMMIT
BaseDatabaseWrapper.set_autocommit(autocommit=True)
DatabaseWrapper._set_autocommit(True)
the connection holds: in_atomic_block=False, savepoint_ids=[], atomic_blocks=[], commit_on_exit=True, needs_rollback=False
connection.get_autocommit() -> True
A block an exception leaves
With an exception leaving, or with needs_rollback set and none leaving, __exit__ first sets needs_rollback to False, to be set again, the comment says, if there is no savepoint to roll back to at this level. An inner block with a savepoint rolls back to it and then releases it, the comment saying the savepoint will not be used again and releasing it spares the database server the overhead. An outermost block that turned autocommit off rolls the transaction back. Here the outer block has created the port Wick and the inner block Thurso, and an exception is handed to the inner block as a with statement hands it:
inner.__exit__(ValueError, ValueError('no berth'), None)
Atomic.__exit__(exc_type=ValueError)
BaseDatabaseWrapper.savepoint_rollback('s(the thread)_x2')
SQL ROLLBACK TO SAVEPOINT "s(the thread)_x2"
sqlite ROLLBACK TO SAVEPOINT "s(the thread)_x2"
BaseDatabaseWrapper.savepoint_commit('s(the thread)_x2')
SQL RELEASE SAVEPOINT "s(the thread)_x2"
sqlite RELEASE SAVEPOINT "s(the thread)_x2"
the connection holds: in_atomic_block=True, savepoint_ids=[], atomic_blocks=[an Atomic], commit_on_exit=True, needs_rollback=False
...
sorted(Port.objects.filter(name__in=['Wick', 'Thurso']).values_list('name', flat=True)) -> ['Wick']
An inner block without a savepoint has nothing of its own to roll back to: it sets needs_rollback, and the rollback passes to the next block out that can do it. Until one does, the connection refuses statements. Here the outer block created the port Lerwick and the unsaved block Kirkwall, in lines left out:
unsaved.__enter__()
Atomic.__enter__ (an Atomic made with savepoint=False)
the connection holds: in_atomic_block=True, savepoint_ids=[None], atomic_blocks=[an Atomic, an Atomic], commit_on_exit=True, needs_rollback=False
unsaved.__exit__(ValueError, ValueError('no berth'), None)
Atomic.__exit__(exc_type=ValueError)
the connection holds: in_atomic_block=True, savepoint_ids=[], atomic_blocks=[an Atomic], commit_on_exit=True, needs_rollback=True
Port.objects.count()
SQL SELECT COUNT(*) AS "__count" FROM "fleet_port"
raises TransactionManagementError: An error occurred in the current transaction. You can't execute queries until the end of the 'atomic' block.
outer.__exit__(None, None, None)
Atomic.__exit__
BaseDatabaseWrapper.rollback
BaseDatabaseWrapper._rollback
sqlite3.Connection.rollback
sqlite ROLLBACK
...
sorted(Port.objects.filter(name__in=['Lerwick', 'Kirkwall']).values_list('name', flat=True)) -> []
The count was refused before it reached SQLite, and the outer block rolled back though no exception left it, taking with it Lerwick as well as Kirkwall.
When the release, the commit or the rollback fails
A release that raises DatabaseError is answered with a rollback to the savepoint and a release, and the DatabaseError is raised again; where those raise too, needs_rollback is set, which the comment says marks the transaction for a rollback at a higher level without hiding the first exception, and a failed rollback to a savepoint in the exception branch sets it too. A commit that raises DatabaseError is answered with a rollback before it is raised again. Where a rollback of the transaction raises, there or in the exception branch, the connection is closed (SQLite’s backend ignores this for an in-memory database), the comment saying that an error during a rollback means something went wrong with the connection, and the finally’s set_autocommit(True), finding no driver’s connection, opens a new one. Here a stand-in for the driver’s connection raises at the commit and at the rollback; the calls of the block’s entry and query are left out:
with transaction.atomic(): Port.objects.count()
Atomic.__exit__
BaseDatabaseWrapper.commit
BaseDatabaseWrapper._commit
FailingDriverConnection.commit raises sqlite3.OperationalError
raises OperationalError
BaseDatabaseWrapper.rollback
BaseDatabaseWrapper._rollback
FailingDriverConnection.rollback raises sqlite3.OperationalError
raises OperationalError
DatabaseWrapper.close
...
BaseDatabaseWrapper.set_autocommit(autocommit=True)
BaseDatabaseWrapper.connect
...
raises OperationalError: disk I/O error
needs_rollback: a transaction marked broken
BaseDatabaseWrapper marks a transaction as past use, as one in which a statement has failed may be. Django’s documentation says that on PostgreSQL every later statement in it fails until the transaction ends, and that after a DatabaseError inside a block the transaction is broken and is rolled back at the block’s end. Django refuses such statements itself, on every backend: CursorWrapper, CursorWrapper and CursorWrapper each call BaseDatabaseWrapper before the statement reaches the driver, and while needs_rollback is set it raises TransactionManagementError from BaseDatabaseWrapper.
- The mark is set by an exit with no savepoint to roll back to, by the failure of a rollback to a savepoint or of the release that follows one, by
BaseDatabaseWrapperinside a block, by.close set_rollback(True)and bymark_for_rollback_on_error. - It is cleared by a rollback: the exit of a block with a savepoint or of an outermost block that turned autocommit off, or
BaseDatabaseWrapperunder autocommit turned off by hand..rollback set_rollback(False)clears it without one, and so do an outermost block’s entry andBaseDatabaseWrapper..connect
Catching the exception inside the block does not clear it. Here a create inside a block makes a second ship named Petrel at the first one’s home port, which a unique constraint on a ship’s name and home port forbids, and the exception is caught there:
Ship.objects.create(name='Petrel', tonnage=480, home_id=petrel.home_id)
mark_for_rollback_on_error
mark_for_rollback_on_error resumes
raises IntegrityError
raises IntegrityError: UNIQUE constraint failed: fleet_ship.name, fleet_ship.home_id
connection.needs_rollback -> True
type(connection.rollback_exc) -> django.db.utils.IntegrityError
Port.objects.count()
SQL SELECT COUNT(*) AS "__count" FROM "fleet_port"
raises TransactionManagementError: An error occurred in the current transaction. You can't execute queries until the end of the 'atomic' block.
type(caught) -> django.db.transaction.TransactionManagementError
type(caught.__cause__) -> django.db.utils.IntegrityError
Only mark_for_rollback_on_error sets rollback_exc, and nothing sets it back to None: where an exit set the mark, as the unsaved block’s did above, the refusal gives as its cause whatever exception was stored last, in this transaction or an earlier one.
mark_for_rollback_on_error
mark_for_rollback_on_error is a context manager that opens nothing: it lets an exception pass through and, on its way, where it is an Exception and an atomic block is open, sets BaseDatabaseWrapper and stores the exception as BaseDatabaseWrapper. Its docstring gives it as equivalent to running the code bare under autocommit and inside atomic(savepoint=False) otherwise, and says Model.save and its kin need it to avoid starting a transaction under autocommit when a single query is executed. Its callers are Model.save_base for a model without parents, QuerySet.update, and Collector.delete where it deletes one object that can be deleted fast.
set_rollback and get_rollback
BaseDatabaseWrapper sets BaseDatabaseWrapper by hand and BaseDatabaseWrapper returns it; both raise TransactionManagementError outside an atomic block, with the message that the rollback flag does not work outside one. The docstring of django says what each value is for: True makes the innermost enclosing block made with savepoint=True, the default, roll back as it exits, without an exception; False prevents that rollback, and is to be used only after rolling back to a known-good state, as otherwise the block is broken and data may be corrupted. In an outermost block, with no exception:
transaction.set_rollback(True)
the connection holds: in_atomic_block=True, savepoint_ids=[], atomic_blocks=[an Atomic], commit_on_exit=True, needs_rollback=True
outer.__exit__(None, None, None)
Atomic.__exit__
BaseDatabaseWrapper.rollback
BaseDatabaseWrapper._rollback
sqlite3.Connection.rollback
sqlite ROLLBACK
Durable blocks
atomic(durable=True) makes a block that must be outermost: Atomic.__enter__ raises RuntimeError where a block is already open, before it changes anything. The docstring gives what this guarantees, that the changes made in a durable block are committed when it exits without an error, which holds where autocommit was on as the block was entered and nothing has marked the transaction for rollback. The outer block, entered first, is left by the exception and rolls back:
with transaction.atomic(), transaction.atomic(durable=True): pass
Atomic.__enter__
BaseDatabaseWrapper.set_autocommit(autocommit=False, force_begin_transaction_with_broken_autocommit=True)
DatabaseWrapper._start_transaction_under_autocommit
SQL BEGIN
sqlite BEGIN
Atomic.__enter__ (an Atomic made with durable=True) raises RuntimeError
Atomic.__exit__(exc_type=RuntimeError)
BaseDatabaseWrapper.rollback
BaseDatabaseWrapper._rollback
sqlite3.Connection.rollback
sqlite ROLLBACK
BaseDatabaseWrapper.set_autocommit(autocommit=True)
DatabaseWrapper._set_autocommit(True)
raises RuntimeError: A durable atomic block cannot be nested within another atomic block.
The check looks only at the innermost open block, and lets the durable block through where that block’s Atomic is set. TestCase sets it on the blocks it opens around a class of tests and around each test, so a durable block that code under a test opens directly inside them is let through, and one opened inside another block of the test’s code is refused (A transaction around each test: TestCase).
Autocommit turned off by hand
django turns autocommit off with no block open, and leaves the transaction to the code: Django sends nothing to begin it, and the code ends it with commit or rollback. On SQLite the driver’s isolation_level becomes the empty string, and the driver sends a "BEGIN" of its own before the first statement that changes a row:
transaction.set_autocommit(False)
BaseDatabaseWrapper.set_autocommit(autocommit=False)
debug_transaction(connection, 'BEGIN')
DatabaseWrapper._set_autocommit(False)
debug_transaction resumes
connection.connection.isolation_level -> ''
Port.objects.create(name='Mallaig', country='Scotland')
SQL INSERT INTO "fleet_port" ("name", "country") VALUES (%s, %s) RETURNING "fleet_port"."id" with params ('Mallaig', 'Scotland')
sqlite BEGIN
sqlite INSERT INTO "fleet_port" ("name", "country") VALUES ('Mallaig', 'Scotland') RETURNING "fleet_port"."id"
An atomic block entered later, with autocommit still off, pretends, as its entry does in that case: BaseDatabaseWrapper is False, and even the outermost block makes a savepoint, unless it was made with savepoint=False, and releases it as it ends, leaving the commit of the driver’s transaction to the code:
outer.__enter__()
Atomic.__enter__
BaseDatabaseWrapper.savepoint
SQL SAVEPOINT "s(the thread)_x5"
sqlite SAVEPOINT "s(the thread)_x5"
-> 's(the thread)_x5'
the connection holds: in_atomic_block=True, savepoint_ids=['s(the thread)_x5'], atomic_blocks=[an Atomic], commit_on_exit=False, needs_rollback=False
...
outer.__exit__(None, None, None)
Atomic.__exit__
BaseDatabaseWrapper.savepoint_commit('s(the thread)_x5')
SQL RELEASE SAVEPOINT "s(the thread)_x5"
sqlite RELEASE SAVEPOINT "s(the thread)_x5"
the connection holds: in_atomic_block=False, savepoint_ids=[], atomic_blocks=[], commit_on_exit=False, needs_rollback=False
Django’s documentation asks the code to end the transaction before it turns autocommit on again, and BaseDatabaseWrapper closes a connection whose autocommit differs from its alias’s "AUTOCOMMIT" at a request’s start and end (Opening and closing a connection). An alias with "AUTOCOMMIT" set to False opens every connection in this state. on_commit refuses here outside a block (Work held until the commit: on_commit).
"ATOMIC_REQUESTS" and non_atomic_requests
"ATOMIC_REQUESTS" is False unless an alias says otherwise. Where it is True, BaseHandler wraps the view in atomic for that alias before calling it, so that a view that returns commits its work and one that raises rolls it back; the handler’s side is in The centre of the chain. non_atomic_requests marks a view to be left out, for "default" when it is used bare and for the alias it is given otherwise: _non_atomic_requests wraps the view in a function that carries the set of aliases to leave out, the view’s own set with the alias added where the view already has one, so the decorator can be stacked for several aliases. Three views, and requests to two of them as far as the last view’s call, with SQLite’s own lines and the driver’s rollback left out:
# the views of the script's own:
def write(request):
Port.objects.create(name="Scrabster", country="Scotland")
return HttpResponse("written")
def fail(request):
Port.objects.create(name="Stornoway", country="Scotland")
raise ValueError("the view failed")
@transaction.non_atomic_requests
def plain(request):
Port.objects.create(name="Kyle", country="Scotland")
return HttpResponse("written")
settings.ROOT_URLCONF = (path('write/', write), path('fail/', fail), path('plain/', plain))
...
response = handler.get_response(RequestFactory().get('/fail/'))
BaseHandler.make_view_atomic(fail) -> a function that calls fail inside an Atomic (made by contextlib's ContextDecorator, Atomic's base class)
Atomic.__enter__
SQL BEGIN
fail(request)
SQL INSERT INTO "fleet_port" ("name", "country") VALUES (%s, %s) RETURNING "fleet_port"."id" with params ('Stornoway', 'Scotland')
raises ValueError
Atomic.__exit__(exc_type=ValueError)
BaseDatabaseWrapper.rollback
response.status_code -> 500
response = handler.get_response(RequestFactory().get('/plain/'))
BaseHandler.make_view_atomic(plain) -> plain as it was given
plain(request)
The marked view is called as it was given, so its insert runs under autocommit, a transaction of its own.