Skip to content

Commit 18877c4

Browse files
committed
progress
1 parent 537cf2e commit 18877c4

File tree

7 files changed

+225
-253
lines changed

7 files changed

+225
-253
lines changed

django_mongodb_backend/base.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -282,14 +282,7 @@ def get_database_version(self):
282282
return tuple(self.connection.server_info()["versionArray"])
283283

284284
@requires_transaction_support
285-
def _start_transaction(self, autocommit, force_begin_transaction_with_broken_autocommit=False):
286-
# Besides @transaction.atomic() (which uses
287-
# _start_transaction_under_autocommit(), disabling autocommit is
288-
# another way to start a transaction.
289-
# if not autocommit:
290-
# self._start_transaction()
291-
# def _start_transaction(self):
292-
# Private API, specific to this backend.
285+
def _start_transaction(self):
293286
if self.session is None:
294287
self.session = self.connection.start_session()
295288
with debug_transaction(self, "session.start_transaction()"):

django_mongodb_backend/transaction.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
from contextlib import ContextDecorator
22

3-
from django.db import (
4-
DEFAULT_DB_ALIAS,
5-
DatabaseError,
6-
Error,
7-
)
3+
from django.db import DEFAULT_DB_ALIAS, DatabaseError, Error
84
from django.db.transaction import get_connection
95

106

@@ -16,11 +12,6 @@ def on_commit(func, using=None, robust=False):
1612
get_connection(using).on_commit(func, robust)
1713

1814

19-
#################################
20-
# Decorators / context managers #
21-
#################################
22-
23-
2415
class Atomic(ContextDecorator):
2516
"""
2617
Guarantee the atomic execution of a given block.
@@ -44,14 +35,11 @@ class Atomic(ContextDecorator):
4435
raised if it's nested within another atomic block. This guarantees
4536
that database changes in a durable block are committed to the database when
4637
the block exits without error.
47-
48-
This is a private API.
4938
"""
5039

5140
def __init__(self, using, durable):
5241
self.using = using
5342
self.durable = durable
54-
self._from_testcase = False
5543

5644
def __enter__(self):
5745
connection = get_connection(self.using)
@@ -75,9 +63,7 @@ def __enter__(self):
7563
# We're already in a transaction. Increment the number of nested atomics.
7664
connection.nested_atomics += 1
7765
else:
78-
connection._start_transaction(
79-
False, force_begin_transaction_with_broken_autocommit=True
80-
)
66+
connection._start_transaction()
8167
connection.in_atomic_block_mongo = True
8268

8369
if connection.in_atomic_block_mongo:

docs/source/releases/5.2.x.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ New features
1313
- Added subquery support for :class:`~.fields.EmbeddedModelArrayField`.
1414
- Added the ``options`` parameter to
1515
:func:`~django_mongodb_backend.utils.parse_uri`.
16+
- Added support for :ref:`database transactions <transactions>`.
1617
- Added :class:`~.fields.PolymorphicEmbeddedModelField` and
1718
:class:`~.fields.PolymorphicEmbeddedModelArrayField` for storing a model
1819
instance or list of model instances that may be of more than one model class.

docs/source/topics/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ know:
1010

1111
cache
1212
embedded-models
13+
transactions
1314
known-issues

docs/source/topics/known-issues.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ Query execution uses Django and MongoDB's default behavior of autocommit mode.
8484
Each query is immediately committed to the database.
8585

8686
Django's :doc:`transaction management APIs <django:topics/db/transactions>`
87-
are not supported.
87+
are not supported. Instead, this package provides its own :doc:`transaction APIs
88+
<topics/transactions>`.
8889

8990
Database introspection
9091
======================

docs/source/topics/transactions.rst

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
============
2+
Transactions
3+
============
4+
5+
.. versionadded:: 5.2.0b2
6+
7+
.. module:: django_mongod_backend.transaction
8+
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>`.
11+
12+
Because MongoDB transactions have some differences compared to SQL transactions,
13+
:doc:`Django's transactions APIs <django:topics/db/transactions>` function as no-ops.
14+
15+
Instead, Django MongoDB Backend provides its own ``atomic()`` function, similar to
16+
Django's :func:`django.db.transaction.atomic`.
17+
18+
Outside of a transaction, query execution uses Django and MongoDB's default behavior of
19+
autocommit mode. Each query is immediately committed to the database.
20+
21+
Controlling transactions
22+
------------------------
23+
24+
.. function:: atomic(using=None, durable=False)
25+
26+
Atomicity is the defining property of database transactions. ``atomic``
27+
allows us to create a block of code within which the atomicity on the
28+
database is guaranteed. If the block of code is successfully completed, the
29+
changes are committed to the database. If there is an exception, the
30+
changes are rolled back.
31+
32+
``atomic`` blocks can be nested. In this case, when an inner block
33+
completes successfully, its effects can still be rolled back if an
34+
exception is raised in the outer block at a later point.
35+
36+
It is sometimes useful to ensure an ``atomic`` block is always the
37+
outermost ``atomic`` block, ensuring that any database changes are
38+
committed when the block is exited without errors. This is known as
39+
durability and can be achieved by setting ``durable=True``. If the
40+
``atomic`` block is nested within another it raises a ``RuntimeError``.
41+
42+
``atomic`` is usable both as a :py:term:`decorator`::
43+
44+
from django.db import transaction
45+
46+
47+
@transaction.atomic
48+
def viewfunc(request):
49+
# This code executes inside a transaction.
50+
do_stuff()
51+
52+
and as a :py:term:`context manager`::
53+
54+
from django.db import transaction
55+
56+
57+
def viewfunc(request):
58+
# This code executes in autocommit mode (Django's default).
59+
do_stuff()
60+
61+
with transaction.atomic():
62+
# This code executes inside a transaction.
63+
do_more_stuff()
64+
65+
.. admonition:: Avoid catching exceptions inside ``atomic``!
66+
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
71+
can result in unexpected behavior.
72+
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. If you attempt to run database
77+
queries before the rollback happens, Django will raise a
78+
:class:`~django.db.transaction.TransactionManagementError`. You may
79+
also encounter this behavior when an ORM-related signal handler raises
80+
an exception.
81+
82+
The correct way to catch database errors is around an ``atomic`` block
83+
as shown above. If necessary, add an extra ``atomic`` block for this
84+
purpose. This pattern has another advantage: it delimits explicitly
85+
which operations will be rolled back if an exception occurs.
86+
87+
If you catch exceptions raised by raw SQL queries, Django's behavior
88+
is unspecified and database-dependent.
89+
90+
.. admonition:: You may need to manually revert app state when rolling back a transaction.
91+
92+
The values of a model's fields won't be reverted when a transaction
93+
rollback happens. This could lead to an inconsistent model state unless
94+
you manually restore the original field values.
95+
96+
For example, given ``MyModel`` with an ``active`` field, this snippet
97+
ensures that the ``if obj.active`` check at the end uses the correct
98+
value if updating ``active`` to ``True`` fails in the transaction::
99+
100+
from django.db import DatabaseError, transaction
101+
102+
obj = MyModel(active=False)
103+
obj.active = True
104+
try:
105+
with transaction.atomic():
106+
obj.save()
107+
except DatabaseError:
108+
obj.active = False
109+
110+
if obj.active:
111+
...
112+
113+
This also applies to any other mechanism that may hold app state, such
114+
as caching or global variables. For example, if the code proactively
115+
updates data in the cache after saving an object, it's recommended to
116+
use :ref:`transaction.on_commit() <performing-actions-after-commit>`
117+
instead, to defer cache alterations until the transaction is actually
118+
committed.
119+
120+
In order to guarantee atomicity, ``atomic`` disables some APIs. Attempting
121+
to commit, roll back, or change the autocommit state of the database
122+
connection within an ``atomic`` block will raise an exception.
123+
124+
``atomic`` takes a ``using`` argument which should be the name of a
125+
database. If this argument isn't provided, Django uses the ``"default"``
126+
database.
127+
128+
Under the hood, Django's transaction management code:
129+
130+
- opens a transaction when entering the outermost ``atomic`` block;
131+
- commits or rolls back the transaction when exiting the outermost block.
132+
133+
.. admonition:: Performance considerations
134+
135+
Open transactions have a performance cost for your database server. To
136+
minimize this overhead, keep your transactions as short as possible. This
137+
is especially important if you're using :func:`atomic` in long-running
138+
processes, outside of Django's request / response cycle.
139+
140+
.. _transactions-limitations:
141+
142+
Limitations
143+
-----------
144+
145+
MongoDB's transaction limitations that are applicable to Django are:
146+
147+
- :meth:`QuerySet.union() <django.db.models.query.QuerySet.union>` is not
148+
supported inside a transaction.
149+
- If a transaction raises an exception, the transaction is no longer usable.
150+
For example, if the update stage of :meth:`QuerySet.update_or_create()
151+
<django.db.models.query.QuerySet.update_or_create>` fails with
152+
:class:`~django.db.IntegrityError` due to a unique constraint violation, the
153+
create stage won't be able to proceed.
154+
:class:`pymongo.errors.OperationFailure` is raised, wrapped by
155+
:class:`django.db.DatabaseError`.
156+
- Savepoints (i.e. nested :func:`~django.db.transaction.atomic` blocks) aren't
157+
supported. The outermost :func:`~django.db.transaction.atomic` will start
158+
a transaction while any subsequent :func:`~django.db.transaction.atomic`
159+
blocks will have no effect.
160+
- Migration operations aren't :ref:`wrapped in a transaction
161+
<topics/migrations:transactions>` because of MongoDB restrictions such as
162+
adding indexes to existing collections while in a transaction.

0 commit comments

Comments
 (0)