Skip to content

Commit bfd3d8a

Browse files
committed
INTPYTHON-355 Revert transaction support
MongoDB's best practices for transactions ("use them only when needed") isn't compatible with how Django uses atomic() internally. Instead, a separate atomic() function for use in user code will be provided. This reverts commit 4ca6c90.
1 parent 60811cb commit bfd3d8a

File tree

13 files changed

+41
-178
lines changed

13 files changed

+41
-178
lines changed

.github/workflows/test-python-atlas.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@ jobs:
5353
working-directory: .
5454
run: bash .github/workflows/start_local_atlas.sh mongodb/mongodb-atlas-local:7
5555
- name: Run tests
56-
run: python3 django_repo/tests/runtests.py --settings mongodb_settings -v 2
56+
run: python3 django_repo/tests/runtests_.py

django_mongodb_backend/base.py

Lines changed: 4 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
import os
33

44
from django.core.exceptions import ImproperlyConfigured
5-
from django.db import DEFAULT_DB_ALIAS
65
from django.db.backends.base.base import BaseDatabaseWrapper
7-
from django.db.backends.utils import debug_transaction
86
from django.utils.asyncio import async_unsafe
97
from django.utils.functional import cached_property
108
from pymongo.collection import Collection
@@ -34,17 +32,6 @@ def __exit__(self, exception_type, exception_value, exception_traceback):
3432
pass
3533

3634

37-
def requires_transaction_support(func):
38-
"""Make a method a no-op if transactions aren't supported."""
39-
40-
def wrapper(self, *args, **kwargs):
41-
if not self.features.supports_transactions:
42-
return
43-
func(self, *args, **kwargs)
44-
45-
return wrapper
46-
47-
4835
class DatabaseWrapper(BaseDatabaseWrapper):
4936
data_types = {
5037
"AutoField": "int",
@@ -155,10 +142,6 @@ def _isnull_operator(a, b):
155142
ops_class = DatabaseOperations
156143
validation_class = DatabaseValidation
157144

158-
def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS):
159-
super().__init__(settings_dict, alias=alias)
160-
self.session = None
161-
162145
def get_collection(self, name, **kwargs):
163146
collection = Collection(self.database, name, **kwargs)
164147
if self.queries_logged:
@@ -208,48 +191,14 @@ def _driver_info(self):
208191
return DriverInfo("django-mongodb-backend", django_mongodb_backend_version)
209192
return None
210193

211-
@requires_transaction_support
212194
def _commit(self):
213-
if self.session:
214-
with debug_transaction(self, "session.commit_transaction()"):
215-
self.session.commit_transaction()
216-
self._end_session()
195+
pass
217196

218-
@requires_transaction_support
219197
def _rollback(self):
220-
if self.session:
221-
with debug_transaction(self, "session.abort_transaction()"):
222-
self.session.abort_transaction()
223-
self._end_session()
224-
225-
def _start_transaction(self):
226-
# Private API, specific to this backend.
227-
if self.session is None:
228-
self.session = self.connection.start_session()
229-
with debug_transaction(self, "session.start_transaction()"):
230-
self.session.start_transaction()
231-
232-
def _end_session(self):
233-
# Private API, specific to this backend.
234-
self.session.end_session()
235-
self.session = None
236-
237-
@requires_transaction_support
238-
def _start_transaction_under_autocommit(self):
239-
# Implementing this hook (intended only for SQLite), allows
240-
# BaseDatabaseWrapper.set_autocommit() to use it to start a transaction
241-
# rather than set_autocommit(), bypassing set_autocommit()'s call to
242-
# debug_transaction(self, "BEGIN") which isn't semantic for a no-SQL
243-
# backend.
244-
self._start_transaction()
198+
pass
245199

246-
@requires_transaction_support
247-
def _set_autocommit(self, autocommit, force_begin_transaction_with_broken_autocommit=False):
248-
# Besides @transaction.atomic() (which uses
249-
# _start_transaction_under_autocommit(), disabling autocommit is
250-
# another way to start a transaction.
251-
if not autocommit:
252-
self._start_transaction()
200+
def set_autocommit(self, autocommit, force_begin_transaction_with_broken_autocommit=False):
201+
self.autocommit = autocommit
253202

254203
def _close(self):
255204
# Normally called by close(), this method is also called by some tests.
@@ -263,10 +212,6 @@ def close(self):
263212

264213
def close_pool(self):
265214
"""Close the MongoClient."""
266-
# Clear commit hooks and session.
267-
self.run_on_commit = []
268-
if self.session:
269-
self._end_session()
270215
connection = self.connection
271216
if connection is None:
272217
return
@@ -282,10 +227,6 @@ def close_pool(self):
282227
def cursor(self):
283228
return Cursor()
284229

285-
@requires_transaction_support
286-
def validate_no_broken_transaction(self):
287-
super().validate_no_broken_transaction()
288-
289230
def get_database_version(self):
290231
"""Return a tuple of the database's version."""
291232
return tuple(self.connection.server_info()["versionArray"])

django_mongodb_backend/compiler.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -678,10 +678,7 @@ def execute_sql(self, returning_fields=None):
678678
@wrap_database_errors
679679
def insert(self, docs, returning_fields=None):
680680
"""Store a list of documents using field columns as element names."""
681-
self.connection.validate_no_broken_transaction()
682-
inserted_ids = self.collection.insert_many(
683-
docs, session=self.connection.session
684-
).inserted_ids
681+
inserted_ids = self.collection.insert_many(docs).inserted_ids
685682
return [(x,) for x in inserted_ids] if returning_fields else []
686683

687684
@cached_property
@@ -764,10 +761,7 @@ def execute_sql(self, result_type):
764761

765762
@wrap_database_errors
766763
def update(self, criteria, pipeline):
767-
self.connection.validate_no_broken_transaction()
768-
return self.collection.update_many(
769-
criteria, pipeline, session=self.connection.session
770-
).matched_count
764+
return self.collection.update_many(criteria, pipeline).matched_count
771765

772766
def check_query(self):
773767
super().check_query()

django_mongodb_backend/features.py

Lines changed: 17 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
3636
supports_temporal_subtraction = True
3737
# MongoDB stores datetimes in UTC.
3838
supports_timezones = False
39+
# While MongoDB supports transactions in some configurations, this backend will not
40+
# support the transaction.atomic() API.
41+
supports_transactions = False
3942
supports_unspecified_pk = True
4043
uses_savepoints = False
4144

@@ -92,47 +95,13 @@ class DatabaseFeatures(BaseDatabaseFeatures):
9295
"expressions.tests.ExpressionOperatorTests.test_lefthand_bitwise_xor_right_null",
9396
"expressions.tests.ExpressionOperatorTests.test_lefthand_transformed_field_bitwise_or",
9497
}
95-
_django_test_expected_failures_no_transactions = {
96-
# "Save with update_fields did not affect any rows." instead of
97-
# "An error occurred in the current transaction. You can't execute
98-
# queries until the end of the 'atomic' block."
99-
"basic.tests.SelectOnSaveTests.test_select_on_save_lying_update",
100-
}
101-
_django_test_expected_failures_transactions = {
102-
# When update_or_create() fails with IntegrityError, the transaction
103-
# is no longer usable.
104-
"get_or_create.tests.UpdateOrCreateTests.test_manual_primary_key_test",
105-
"get_or_create.tests.UpdateOrCreateTestsWithManualPKs.test_create_with_duplicate_primary_key",
106-
# Tests that require savepoints
107-
"admin_views.tests.AdminViewBasicTest.test_disallowed_to_field",
108-
"admin_views.tests.AdminViewPermissionsTest.test_add_view",
109-
"admin_views.tests.AdminViewPermissionsTest.test_change_view",
110-
"admin_views.tests.AdminViewPermissionsTest.test_change_view_save_as_new",
111-
"admin_views.tests.AdminViewPermissionsTest.test_delete_view",
112-
"auth_tests.test_views.ChangelistTests.test_view_user_password_is_readonly",
113-
"fixtures.tests.FixtureLoadingTests.test_loaddata_app_option",
114-
"fixtures.tests.FixtureLoadingTests.test_unmatched_identifier_loading",
115-
"fixtures_model_package.tests.FixtureTestCase.test_loaddata",
116-
"get_or_create.tests.GetOrCreateTests.test_get_or_create_invalid_params",
117-
"get_or_create.tests.UpdateOrCreateTests.test_integrity",
118-
"many_to_many.tests.ManyToManyTests.test_add",
119-
"many_to_one.tests.ManyToOneTests.test_fk_assignment_and_related_object_cache",
120-
"model_fields.test_booleanfield.BooleanFieldTests.test_null_default",
121-
"model_fields.test_floatfield.TestFloatField.test_float_validates_object",
122-
"multiple_database.tests.QueryTestCase.test_generic_key_cross_database_protection",
123-
"multiple_database.tests.QueryTestCase.test_m2m_cross_database_protection",
124-
}
12598

12699
@cached_property
127100
def django_test_expected_failures(self):
128101
expected_failures = super().django_test_expected_failures
129102
expected_failures.update(self._django_test_expected_failures)
130103
if not self.is_mongodb_6_3:
131104
expected_failures.update(self._django_test_expected_failures_bitwise)
132-
if self.supports_transactions:
133-
expected_failures.update(self._django_test_expected_failures_transactions)
134-
else:
135-
expected_failures.update(self._django_test_expected_failures_no_transactions)
136105
return expected_failures
137106

138107
django_test_skips = {
@@ -515,6 +484,17 @@ def django_test_expected_failures(self):
515484
"Connection health checks not implemented.": {
516485
"backends.base.test_base.ConnectionHealthChecksTests",
517486
},
487+
"transaction.atomic() is not supported.": {
488+
"backends.base.test_base.DatabaseWrapperLoggingTests",
489+
"basic.tests.SelectOnSaveTests.test_select_on_save_lying_update",
490+
"migrations.test_executor.ExecutorTests.test_atomic_operation_in_non_atomic_migration",
491+
"migrations.test_operations.OperationTests.test_run_python_atomic",
492+
},
493+
"transaction.rollback() is not supported.": {
494+
"transactions.tests.AtomicMiscTests.test_mark_for_rollback_on_error_in_autocommit",
495+
"transactions.tests.AtomicMiscTests.test_mark_for_rollback_on_error_in_transaction",
496+
"transactions.tests.NonAutocommitTests.test_orm_query_after_error_and_rollback",
497+
},
518498
"migrate --fake-initial is not supported.": {
519499
"migrations.test_commands.MigrateTests.test_migrate_fake_initial",
520500
"migrations.test_commands.MigrateTests.test_migrate_fake_split_initial",
@@ -609,15 +589,11 @@ def supports_atlas_search(self):
609589
return True
610590

611591
@cached_property
612-
def supports_select_union(self):
613-
# Stage not supported inside of a multi-document transaction: $unionWith
614-
return not self.supports_transactions
615-
616-
@cached_property
617-
def supports_transactions(self):
592+
def _supports_transactions(self):
618593
"""
619594
Transactions are enabled if MongoDB is configured as a replica set or a
620-
sharded cluster.
595+
sharded cluster. It's prefixed with an underscore because this backend does
596+
the usual Django API: transaction.atomic().
621597
"""
622598
self.connection.ensure_connection()
623599
client = self.connection.connection.admin

django_mongodb_backend/query.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,23 +61,17 @@ def __repr__(self):
6161
@wrap_database_errors
6262
def delete(self):
6363
"""Execute a delete query."""
64-
self.compiler.connection.validate_no_broken_transaction()
6564
if self.compiler.subqueries:
6665
raise NotSupportedError("Cannot use QuerySet.delete() when a subquery is required.")
67-
return self.compiler.collection.delete_many(
68-
self.match_mql, session=self.compiler.connection.session
69-
).deleted_count
66+
return self.compiler.collection.delete_many(self.match_mql).deleted_count
7067

7168
@wrap_database_errors
7269
def get_cursor(self):
7370
"""
7471
Return a pymongo CommandCursor that can be iterated on to give the
7572
results of the query.
7673
"""
77-
self.compiler.connection.validate_no_broken_transaction()
78-
return self.compiler.collection.aggregate(
79-
self.get_pipeline(), session=self.compiler.connection.session
80-
)
74+
return self.compiler.collection.aggregate(self.get_pipeline())
8175

8276
def get_pipeline(self):
8377
pipeline = []

django_mongodb_backend/queryset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def __init__(self, pipeline, using, model):
3535
def _execute_query(self):
3636
connection = connections[self.using]
3737
collection = connection.get_collection(self.model._meta.db_table)
38-
self.cursor = collection.aggregate(self.pipeline, session=connection.session)
38+
self.cursor = collection.aggregate(self.pipeline)
3939

4040
def __str__(self):
4141
return str(self.pipeline)

docs/source/ref/database.rst

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -41,43 +41,3 @@ effect. Rather, if you need to close the connection pool, use
4141
.. versionadded:: 5.2.0b0
4242

4343
Support for connection pooling and ``connection.close_pool()`` were added.
44-
45-
.. _transactions:
46-
47-
Transactions
48-
============
49-
50-
.. versionadded:: 5.2.0b2
51-
52-
Support for :doc:`Django's transactions APIs <django:topics/db/transactions>`
53-
is enabled if MongoDB is configured as a :doc:`replica set<manual:replication>`
54-
or a :doc:`sharded cluster <manual:sharding>`.
55-
56-
If transactions aren't supported, query execution uses Django and MongoDB's
57-
default behavior of autocommit mode. Each query is immediately committed to the
58-
database. Django's transaction management APIs, such as
59-
:func:`~django.db.transaction.atomic`, function as no-ops.
60-
61-
.. _transactions-limitations:
62-
63-
Limitations
64-
-----------
65-
66-
MongoDB's transaction limitations that are applicable to Django are:
67-
68-
- :meth:`QuerySet.union() <django.db.models.query.QuerySet.union>` is not
69-
supported inside a transaction.
70-
- If a transaction raises an exception, the transaction is no longer usable.
71-
For example, if the update stage of :meth:`QuerySet.update_or_create()
72-
<django.db.models.query.QuerySet.update_or_create>` fails with
73-
:class:`~django.db.IntegrityError` due to a unique constraint violation, the
74-
create stage won't be able to proceed.
75-
:class:`pymongo.errors.OperationFailure` is raised, wrapped by
76-
:class:`django.db.DatabaseError`.
77-
- Savepoints (i.e. nested :func:`~django.db.transaction.atomic` blocks) aren't
78-
supported. The outermost :func:`~django.db.transaction.atomic` will start
79-
a transaction while any subsequent :func:`~django.db.transaction.atomic`
80-
blocks will have no effect.
81-
- Migration operations aren't :ref:`wrapped in a transaction
82-
<topics/migrations:transactions>` because of MongoDB restrictions such as
83-
adding indexes to existing collections while in a transaction.

docs/source/releases/5.2.x.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ 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>`.
1716
- Added :class:`~.fields.PolymorphicEmbeddedModelField` and
1817
:class:`~.fields.PolymorphicEmbeddedModelArrayField` for storing a model
1918
instance or list of model instances that may be of more than one model class.

docs/source/topics/known-issues.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ Database functions
8080
Transaction management
8181
======================
8282

83-
See :ref:`transactions` for details.
83+
Query execution uses Django and MongoDB's default behavior of autocommit mode.
84+
Each query is immediately committed to the database.
85+
86+
Django's :doc:`transaction management APIs <django:topics/db/transactions>`
87+
are not supported.
8488

8589
Database introspection
8690
======================

tests/backend_/test_base.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.core.exceptions import ImproperlyConfigured
22
from django.db import connection
33
from django.db.backends.signals import connection_created
4-
from django.test import SimpleTestCase, TransactionTestCase
4+
from django.test import SimpleTestCase, TestCase
55

66
from django_mongodb_backend.base import DatabaseWrapper
77

@@ -15,9 +15,7 @@ def test_database_name_empty(self):
1515
DatabaseWrapper(settings).get_connection_params()
1616

1717

18-
class DatabaseWrapperConnectionTests(TransactionTestCase):
19-
available_apps = ["backend_"]
20-
18+
class DatabaseWrapperConnectionTests(TestCase):
2119
def test_set_autocommit(self):
2220
self.assertIs(connection.get_autocommit(), True)
2321
connection.set_autocommit(False)

0 commit comments

Comments
 (0)