-
Notifications
You must be signed in to change notification settings - Fork 27
INTPYTHON-355 Add transaction.atomic() #357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from contextlib import ContextDecorator | ||
|
||
from django.db import DEFAULT_DB_ALIAS, DatabaseError | ||
from django.db.transaction import get_connection, on_commit | ||
|
||
__all__ = [ | ||
"atomic", | ||
"on_commit", # convenience alias | ||
] | ||
|
||
|
||
class Atomic(ContextDecorator): | ||
""" | ||
Guarantee the atomic execution of a given block. | ||
|
||
Simplified from django.db.transaction. | ||
""" | ||
|
||
def __init__(self, using): | ||
self.using = using | ||
|
||
def __enter__(self): | ||
connection = get_connection(self.using) | ||
if connection.in_atomic_block_mongo: | ||
# Track the number of nested atomic() calls. | ||
connection.nested_atomics += 1 | ||
else: | ||
# Start a transaction for the outermost atomic(). | ||
connection.start_transaction_mongo() | ||
connection.in_atomic_block_mongo = True | ||
|
||
def __exit__(self, exc_type, exc_value, traceback): | ||
connection = get_connection(self.using) | ||
if connection.nested_atomics: | ||
# Exiting inner atomic. | ||
connection.nested_atomics -= 1 | ||
else: | ||
# Reset flag when exiting outer atomic. | ||
connection.in_atomic_block_mongo = False | ||
if exc_type is None: | ||
# atomic() exited without an error. | ||
if not connection.in_atomic_block_mongo: | ||
# Commit transaction if outer atomic(). | ||
try: | ||
connection.commit_mongo() | ||
except DatabaseError: | ||
connection.rollback_mongo() | ||
else: | ||
# atomic() exited with an error. | ||
if not connection.in_atomic_block_mongo: | ||
# Rollback transaction if outer atomic(). | ||
connection.rollback_mongo() | ||
|
||
|
||
def atomic(using=None): | ||
# Bare decorator: @atomic -- although the first argument is called `using`, | ||
# it's actually the function being decorated. | ||
if callable(using): | ||
return Atomic(DEFAULT_DB_ALIAS)(using) | ||
# Decorator: @atomic(...) or context manager: with atomic(...): ... | ||
return Atomic(using) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,4 +9,5 @@ know: | |
:maxdepth: 2 | ||
|
||
embedded-models | ||
transactions | ||
known-issues |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
============ | ||
Transactions | ||
============ | ||
|
||
.. versionadded:: 5.2.0b2 | ||
|
||
.. module:: django_mongodb_backend.transaction | ||
|
||
MongoDB supports :doc:`transactions <manual:core/transactions>` if it's | ||
configured as a :doc:`replica set <manual:replication>` or a :doc:`sharded | ||
cluster <manual:sharding>`. | ||
|
||
Because MongoDB transactions have some limitations and are not meant to be used | ||
as freely as SQL transactions, :doc:`Django's transactions APIs | ||
<django:topics/db/transactions>`, including most notably | ||
:func:`django.db.transaction.atomic`, function as no-ops. | ||
|
||
Instead, Django MongoDB Backend provides its own | ||
:func:`django_mongodb_backend.transaction.atomic` function. | ||
|
||
Outside of a transaction, query execution uses Django and MongoDB's default | ||
behavior of autocommit mode. Each query is immediately committed to the | ||
database. | ||
|
||
Controlling transactions | ||
======================== | ||
|
||
.. function:: atomic(using=None) | ||
|
||
Atomicity is the defining property of database transactions. ``atomic`` | ||
allows creating a block of code within which the atomicity on the database | ||
is guaranteed. If the block of code is successfully completed, the changes | ||
are committed to the database. If there is an exception, the changes are | ||
rolled back. | ||
|
||
``atomic`` is usable both as a :py:term:`decorator`:: | ||
|
||
from django_mongodb_backend import transaction | ||
|
||
|
||
@transaction.atomic | ||
def viewfunc(request): | ||
# This code executes inside a transaction. | ||
do_stuff() | ||
|
||
and as a :py:term:`context manager`:: | ||
|
||
from django_mongodb_backend import transaction | ||
|
||
|
||
def viewfunc(request): | ||
# This code executes in autocommit mode (Django's default). | ||
do_stuff() | ||
|
||
with transaction.atomic(): | ||
# This code executes inside a transaction. | ||
do_more_stuff() | ||
|
||
.. admonition:: Avoid catching exceptions inside ``atomic``! | ||
|
||
When exiting an ``atomic`` block, Django looks at whether it's exited | ||
normally or with an exception to determine whether to commit or roll | ||
back. If you catch and handle exceptions inside an ``atomic`` block, | ||
you may hide from Django the fact that a problem has happened. This can | ||
result in unexpected behavior. | ||
|
||
This is mostly a concern for :exc:`~django.db.DatabaseError` and its | ||
subclasses such as :exc:`~django.db.IntegrityError`. After such an | ||
error, the transaction is broken and Django will perform a rollback at | ||
the end of the ``atomic`` block. | ||
|
||
.. admonition:: You may need to manually revert app state when rolling back a transaction. | ||
|
||
The values of a model's fields won't be reverted when a transaction | ||
rollback happens. This could lead to an inconsistent model state unless | ||
you manually restore the original field values. | ||
|
||
For example, given ``MyModel`` with an ``active`` field, this snippet | ||
ensures that the ``if obj.active`` check at the end uses the correct | ||
value if updating ``active`` to ``True`` fails in the transaction:: | ||
|
||
from django_mongodb_backend import transaction | ||
from django.db import DatabaseError | ||
|
||
obj = MyModel(active=False) | ||
obj.active = True | ||
try: | ||
with transaction.atomic(): | ||
obj.save() | ||
except DatabaseError: | ||
obj.active = False | ||
|
||
if obj.active: | ||
... | ||
|
||
This also applies to any other mechanism that may hold app state, such | ||
as caching or global variables. For example, if the code proactively | ||
updates data in the cache after saving an object, it's recommended to | ||
use :ref:`transaction.on_commit() <performing-actions-after-commit>` | ||
instead, to defer cache alterations until the transaction is actually | ||
committed. | ||
|
||
``atomic`` takes a ``using`` argument which should be the name of a | ||
database. If this argument isn't provided, Django uses the ``"default"`` | ||
database. | ||
|
||
.. admonition:: Performance considerations | ||
|
||
Open transactions have a performance cost for your MongoDB server. To | ||
minimize this overhead, keep your transactions as short as possible. This | ||
is especially important if you're using :func:`atomic` in long-running | ||
processes, outside of Django's request / response cycle. | ||
|
||
Performing actions after commit | ||
=============================== | ||
|
||
The :func:`atomic` function supports Django's | ||
:func:`~django.db.transaction.on_commit` API to :ref:`perform actions after a | ||
transaction successfully commits <performing-actions-after-commit>`. | ||
|
||
For convenience, :func:`~django.db.transaction.on_commit` is aliased at | ||
``django_mongodb_backend.transaction.on_commit`` so you can use both:: | ||
|
||
from django_mongodb_backend import transaction | ||
|
||
|
||
transaction.atomic() | ||
transaction.on_commit(...) | ||
|
||
.. _transactions-limitations: | ||
|
||
Limitations | ||
=========== | ||
|
||
MongoDB's transaction limitations that are applicable to Django are: | ||
|
||
- :meth:`QuerySet.union() <django.db.models.query.QuerySet.union>` is not | ||
supported inside a transaction. | ||
- Savepoints (i.e. nested :func:`~django.db.transaction.atomic` blocks) aren't | ||
supported. The outermost :func:`~django.db.transaction.atomic` will start | ||
a transaction while any inner :func:`~django.db.transaction.atomic` blocks | ||
have no effect. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mh, elif could be use with rebuts then else for the rest.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copied from Django, so would rather keep it as is to make incorporating any future changes easier.