Skip to content

Commit d0a066a

Browse files
committed
simplify + reflow docs
1 parent 531cc82 commit d0a066a

File tree

3 files changed

+96
-111
lines changed

3 files changed

+96
-111
lines changed

django_mongodb_backend/base.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,9 @@ def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS):
152152
super().__init__(settings_dict, alias=alias)
153153
self.session = None
154154
# Tracks if the connection is in a transaction managed by
155-
# django_mongodb_backend.transaction.atomic.
155+
# django_mongodb_backend.transaction.atomic. `in_atomic_block` isn't
156+
# used in case Django's atomic() (used internally in Django) is called
157+
# within this package's atomic().
156158
self.in_atomic_block_mongo = False
157159
# Current number of nested 'atomic' calls.
158160
self.nested_atomics = 0
@@ -250,39 +252,46 @@ def get_database_version(self):
250252
"""Return a tuple of the database's version."""
251253
return tuple(self.connection.server_info()["versionArray"])
252254

253-
def _start_transaction(self):
255+
## Transaction API for django_mongodb_backend.transaction.atomic()
256+
@async_unsafe
257+
def start_transaction_mongo(self):
254258
if self.session is None:
255259
self.session = self.connection.start_session()
256260
with debug_transaction(self, "session.start_transaction()"):
257261
self.session.start_transaction()
258262

263+
@async_unsafe
259264
def commit_mongo(self):
260265
if self.session:
261266
with debug_transaction(self, "session.commit_transaction()"):
262267
self.session.commit_transaction()
263268
self._end_session()
264-
self.run_commit_hooks_on_set_autocommit_on = True
269+
self.run_and_clear_commit_hooks()
265270

266271
@async_unsafe
267272
def rollback_mongo(self):
268-
"""Roll back a MongoDB transaction and reset the dirty flag."""
269273
if self.session:
270274
with debug_transaction(self, "session.abort_transaction()"):
271275
self.session.abort_transaction()
272276
self._end_session()
273277
self.run_on_commit = []
274278

275279
def _end_session(self):
276-
# Private API, specific to this backend.
277280
self.session.end_session()
278281
self.session = None
279282

280283
def on_commit(self, func, robust=False):
284+
"""
285+
Copied from BaseDatabaseWrapper.on_commit() except that it checks
286+
in_atomic_block_mongo instead of in_atomic_block.
287+
"""
281288
if not callable(func):
282289
raise TypeError("on_commit()'s callback must be a callable.")
283290
if self.in_atomic_block_mongo:
284291
# Transaction in progress; save for execution on commit.
285-
self.run_on_commit.append((set(self.savepoint_ids), func, robust))
292+
# The first item in the tuple (an empty list) is normally the
293+
# savepoint IDs, which isn't applicable on MongoDB.
294+
self.run_on_commit.append(([], func, robust))
286295
else:
287296
# No transaction in progress; execute immediately.
288297
if robust:

django_mongodb_backend/transaction.py

Lines changed: 23 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,18 @@
11
from contextlib import ContextDecorator
22

33
from django.db import DEFAULT_DB_ALIAS, DatabaseError
4-
from django.db.transaction import get_connection
4+
from django.db.transaction import get_connection, on_commit
55

6-
7-
def on_commit(func, using=None, robust=False):
8-
"""
9-
Register `func` to be called when the current transaction is committed.
10-
If the current transaction is rolled back, `func` will not be called.
11-
"""
12-
get_connection(using).on_commit(func, robust)
6+
__all__ = [
7+
"atomic",
8+
"on_commit", # convenience alias
9+
]
1310

1411

1512
class Atomic(ContextDecorator):
1613
"""
1714
Guarantee the atomic execution of a given block.
1815
19-
An instance can be used either as a decorator or as a context manager.
20-
21-
When it's used as a decorator, __call__ wraps the execution of the
22-
decorated function in the instance itself, used as a context manager.
23-
24-
When it's used as a context manager, __enter__ creates a transaction and
25-
__exit__ commits the transaction on normal exit, and rolls back the transaction on
26-
exceptions.
27-
28-
This allows reentrancy even if the same AtomicWrapper is reused. For
29-
example, it's possible to define `oa = atomic('other')` and use `@oa` or
30-
`with oa:` multiple times.
31-
32-
Since database connections are thread-local, this is thread-safe.
33-
3416
Simplified from django.db.transaction.
3517
"""
3618

@@ -40,51 +22,39 @@ def __init__(self, using):
4022
def __enter__(self):
4123
connection = get_connection(self.using)
4224
if connection.in_atomic_block_mongo:
43-
# If we're already in an atomic(), track the number of nested calls.
25+
# Track the number of nested atomic() calls.
4426
connection.nested_atomics += 1
4527
else:
4628
# Start a transaction for the outermost atomic().
47-
connection._start_transaction()
29+
connection.start_transaction_mongo()
4830
connection.in_atomic_block_mongo = True
4931

5032
def __exit__(self, exc_type, exc_value, traceback):
5133
connection = get_connection(self.using)
5234
if connection.nested_atomics:
35+
# Exiting inner atomic.
5336
connection.nested_atomics -= 1
5437
else:
38+
# Reset flag when exiting outer atomic.
5539
connection.in_atomic_block_mongo = False
56-
try:
57-
if exc_type is None:
58-
# atomic() exited without an error.
59-
if connection.in_atomic_block_mongo:
60-
# Do nothing for an inner atomic().
61-
pass
62-
else:
63-
# Commit transaction.
64-
try:
65-
connection.commit_mongo()
66-
except DatabaseError:
67-
connection.rollback_mongo()
68-
else:
69-
# atomic() exited with an error.
70-
if connection.in_atomic_block_mongo:
71-
# Do nothing for an inner atomic().
72-
pass
73-
else:
74-
# Rollback transaction.
40+
if exc_type is None:
41+
# atomic() exited without an error.
42+
if not connection.in_atomic_block_mongo:
43+
# Commit transaction if outer atomic().
44+
try:
45+
connection.commit_mongo()
46+
except DatabaseError:
7547
connection.rollback_mongo()
76-
finally:
77-
if (
78-
not connection.in_atomic_block_mongo
79-
and connection.run_commit_hooks_on_set_autocommit_on
80-
):
81-
# Run on_commit() callbacks after outermost atomic()
82-
connection.run_and_clear_commit_hooks()
48+
else:
49+
# atomic() exited with an error.
50+
if connection.in_atomic_block_mongo:
51+
# Rollback transaction if outer atomic().
52+
connection.rollback_mongo()
8353

8454

8555
def atomic(using=None):
86-
# Bare decorator: @atomic -- although the first argument is called `using`, it's
87-
# actually the function being decorated.
56+
# Bare decorator: @atomic -- although the first argument is called `using`,
57+
# it's actually the function being decorated.
8858
if callable(using):
8959
return Atomic(DEFAULT_DB_ALIAS)(using)
9060
# Decorator: @atomic(...) or context manager: with atomic(...): ...

docs/source/topics/transactions.rst

Lines changed: 58 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,45 @@ Transactions
66

77
.. module:: django_mongodb_backend.transaction
88

9-
MongoDB supports :doc:`transactions <manual:core/transactions>` if it's configured as a
10-
:doc:`replica set <manual:replication>` or a :doc:`sharded cluster <manual:sharding>`.
9+
MongoDB supports :doc:`transactions <manual:core/transactions>` if it's
10+
configured as a :doc:`replica set <manual:replication>` or a :doc:`sharded
11+
cluster <manual:sharding>`.
1112

12-
Because MongoDB transactions have some limitations and are not meant to be used as
13-
freely as SQL transactions, :doc:`Django's transactions APIs
13+
Because MongoDB transactions have some limitations and are not meant to be used
14+
as freely as SQL transactions, :doc:`Django's transactions APIs
1415
<django:topics/db/transactions>`, including most notably
1516
:func:`django.db.transaction.atomic`, function as no-ops.
1617

1718
Instead, Django MongoDB Backend provides its own
1819
:func:`django_mongodb_backend.transaction.atomic` function.
1920

20-
Outside of a transaction, query execution uses Django and MongoDB's default behavior of
21-
autocommit mode. Each query is immediately committed to the database.
21+
Outside of a transaction, query execution uses Django and MongoDB's default
22+
behavior of autocommit mode. Each query is immediately committed to the
23+
database.
2224

2325
Controlling transactions
2426
========================
2527

2628
.. function:: atomic(using=None)
2729

28-
Atomicity is the defining property of database transactions. ``atomic`` allows
29-
creating a block of code within which the atomicity on the database is guaranteed.
30-
If the block of code is successfully completed, the changes are committed to the
31-
database. If there is an exception, the changes are rolled back.
30+
Atomicity is the defining property of database transactions. ``atomic``
31+
allows creating a block of code within which the atomicity on the database
32+
is guaranteed. If the block of code is successfully completed, the changes
33+
are committed to the database. If there is an exception, the changes are
34+
rolled back.
3235

33-
On databases that support savepoints, ``atomic`` blocks can be nested, such that
34-
the outermost ``atomic`` starts a transaction and inner ``atomic``\s create
35-
savepoints. Since MongoDB doesn't support savepoints, inner ``atomic`` blocks
36-
don't have any effect. The transaction commits once the outermost ``atomic`` exits.
36+
On databases that support savepoints, ``atomic`` blocks can be nested, such
37+
that the outermost ``atomic`` starts a transaction and inner ``atomic``\s
38+
create savepoints. Since MongoDB doesn't support savepoints, inner
39+
``atomic`` blocks don't have any effect. The transaction commits once the
40+
outermost ``atomic`` exits.
3741

3842
``atomic`` is usable both as a :py:term:`decorator`::
3943

4044
from django_mongodb_backend import transaction
4145

4246

43-
@atomic
47+
@transaction.atomic
4448
def viewfunc(request):
4549
# This code executes inside a transaction.
4650
do_stuff()
@@ -54,31 +58,32 @@ Controlling transactions
5458
# This code executes in autocommit mode (Django's default).
5559
do_stuff()
5660

57-
with atomic():
61+
with transaction.atomic():
5862
# This code executes inside a transaction.
5963
do_more_stuff()
6064

6165
.. admonition:: Avoid catching exceptions inside ``atomic``!
6266

63-
When exiting an ``atomic`` block, Django looks at whether it's exited normally
64-
or with an exception to determine whether to commit or roll back. If you catch
65-
and handle exceptions inside an ``atomic`` block, you may hide from Django the
66-
fact that a problem has happened. This can result in unexpected behavior.
67+
When exiting an ``atomic`` block, Django looks at whether it's exited
68+
normally or with an exception to determine whether to commit or roll
69+
back. If you catch and handle exceptions inside an ``atomic`` block,
70+
you may hide from Django the fact that a problem has happened. This can
71+
result in unexpected behavior.
6772

68-
This is mostly a concern for :exc:`~django.db.DatabaseError` and its subclasses
69-
such as :exc:`~django.db.IntegrityError`. After such an error, the transaction
70-
is broken and Django will perform a rollback at the end of the ``atomic``
71-
block.
73+
This is mostly a concern for :exc:`~django.db.DatabaseError` and its
74+
subclasses such as :exc:`~django.db.IntegrityError`. After such an
75+
error, the transaction is broken and Django will perform a rollback at
76+
the end of the ``atomic`` block.
7277

7378
.. admonition:: You may need to manually revert app state when rolling back a transaction.
7479

75-
The values of a model's fields won't be reverted when a transaction rollback
76-
happens. This could lead to an inconsistent model state unless you manually
77-
restore the original field values.
80+
The values of a model's fields won't be reverted when a transaction
81+
rollback happens. This could lead to an inconsistent model state unless
82+
you manually restore the original field values.
7883

79-
For example, given ``MyModel`` with an ``active`` field, this snippet ensures
80-
that the ``if obj.active`` check at the end uses the correct value if updating
81-
``active`` to ``True`` fails in the transaction::
84+
For example, given ``MyModel`` with an ``active`` field, this snippet
85+
ensures that the ``if obj.active`` check at the end uses the correct
86+
value if updating ``active`` to ``True`` fails in the transaction::
8287

8388
from django_mongodb_backend import transaction
8489
from django.db import DatabaseError
@@ -94,11 +99,12 @@ Controlling transactions
9499
if obj.active:
95100
...
96101

97-
This also applies to any other mechanism that may hold app state, such as
98-
caching or global variables. For example, if the code proactively updates data
99-
in the cache after saving an object, it's recommended to use
100-
:ref:`transaction.on_commit() <performing-actions-after-commit>` instead, to
101-
defer cache alterations until the transaction is actually committed.
102+
This also applies to any other mechanism that may hold app state, such
103+
as caching or global variables. For example, if the code proactively
104+
updates data in the cache after saving an object, it's recommended to
105+
use :ref:`transaction.on_commit() <performing-actions-after-commit>`
106+
instead, to defer cache alterations until the transaction is actually
107+
committed.
102108

103109
``atomic`` takes a ``using`` argument which should be the name of a
104110
database. If this argument isn't provided, Django uses the ``"default"``
@@ -111,17 +117,27 @@ Controlling transactions
111117

112118
.. admonition:: Performance considerations
113119

114-
Open transactions have a performance cost for your database server. To minimize
115-
this overhead, keep your transactions as short as possible. This is especially
116-
important if you're using :func:`atomic` in long-running processes, outside of
117-
Django's request / response cycle.
120+
Open transactions have a performance cost for your database server. To
121+
minimize this overhead, keep your transactions as short as possible. This
122+
is especially important if you're using :func:`atomic` in long-running
123+
processes, outside of Django's request / response cycle.
118124

119125
Performing actions after commit
120126
===============================
121127

122-
The :func:`atomic` function supports Django's :func:`~django.db.transaction.on_commit`
123-
API to :ref:`perform actions after a transaction successfully commits
124-
<performing-actions-after-commit>`.
128+
The :func:`atomic` function supports Django's
129+
:func:`~django.db.transaction.on_commit` API to :ref:`perform actions after a
130+
transaction successfully commits <performing-actions-after-commit>`.
131+
132+
For convenience, :func:`~django.db.transaction.on_commit` is aliased at
133+
``django_mongodb_backend.transaction.on_commit`` so you can use both::
134+
135+
from django_mongodb_backend import transaction
136+
137+
138+
transaction.atomic()
139+
transaction.on_commit(...)
140+
125141

126142
.. _transactions-limitations:
127143

@@ -132,17 +148,7 @@ MongoDB's transaction limitations that are applicable to Django are:
132148

133149
- :meth:`QuerySet.union() <django.db.models.query.QuerySet.union>` is not
134150
supported inside a transaction.
135-
- If a transaction raises an exception, the transaction is no longer usable.
136-
For example, if the update stage of :meth:`QuerySet.update_or_create()
137-
<django.db.models.query.QuerySet.update_or_create>` fails with
138-
:class:`~django.db.IntegrityError` due to a unique constraint violation, the
139-
create stage won't be able to proceed.
140-
:class:`pymongo.errors.OperationFailure` is raised, wrapped by
141-
:class:`django.db.DatabaseError`.
142151
- Savepoints (i.e. nested :func:`~django.db.transaction.atomic` blocks) aren't
143152
supported. The outermost :func:`~django.db.transaction.atomic` will start
144153
a transaction while any subsequent :func:`~django.db.transaction.atomic`
145154
blocks will have no effect.
146-
- Migration operations aren't :ref:`wrapped in a transaction
147-
<topics/migrations:transactions>` because of MongoDB restrictions such as
148-
adding indexes to existing collections while in a transaction.

0 commit comments

Comments
 (0)