Skip to content

Commit d5aa1a7

Browse files
committed
INTPYTHON-355 Remove 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 commits 4ca6c90 and c7831c6.
1 parent 6274fae commit d5aa1a7

File tree

13 files changed

+42
-197
lines changed

13 files changed

+42
-197
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,6 @@ 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
5757
permissions:
5858
contents: read

django_mongodb_backend/base.py

Lines changed: 4 additions & 68 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,15 +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-
162-
def check_settings(self):
163-
super().check_settings()
164-
if not self.settings_dict["AUTOCOMMIT"]:
165-
raise ImproperlyConfigured("MongoDB does not support AUTOCOMMIT=False.")
166-
167145
def get_collection(self, name, **kwargs):
168146
collection = Collection(self.database, name, **kwargs)
169147
if self.queries_logged:
@@ -213,48 +191,14 @@ def _driver_info(self):
213191
return DriverInfo("django-mongodb-backend", django_mongodb_backend_version)
214192
return None
215193

216-
@requires_transaction_support
217194
def _commit(self):
218-
if self.session:
219-
with debug_transaction(self, "session.commit_transaction()"):
220-
self.session.commit_transaction()
221-
self._end_session()
195+
pass
222196

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

251-
@requires_transaction_support
252-
def _set_autocommit(self, autocommit, force_begin_transaction_with_broken_autocommit=False):
253-
# Besides @transaction.atomic() (which uses
254-
# _start_transaction_under_autocommit(), disabling autocommit is
255-
# another way to start a transaction.
256-
if not autocommit:
257-
self._start_transaction()
200+
def set_autocommit(self, autocommit, force_begin_transaction_with_broken_autocommit=False):
201+
self.autocommit = autocommit
258202

259203
def _close(self):
260204
# Normally called by close(), this method is also called by some tests.
@@ -268,10 +212,6 @@ def close(self):
268212

269213
def close_pool(self):
270214
"""Close the MongoClient."""
271-
# Clear commit hooks and session.
272-
self.run_on_commit = []
273-
if self.session:
274-
self._end_session()
275215
connection = self.connection
276216
if connection is None:
277217
return
@@ -287,10 +227,6 @@ def close_pool(self):
287227
def cursor(self):
288228
return Cursor()
289229

290-
@requires_transaction_support
291-
def validate_no_broken_transaction(self):
292-
super().validate_no_broken_transaction()
293-
294230
def get_database_version(self):
295231
"""Return a tuple of the database's version."""
296232
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
@@ -762,10 +759,7 @@ def execute_sql(self, result_type):
762759

763760
@wrap_database_errors
764761
def update(self, criteria, pipeline):
765-
self.connection.validate_no_broken_transaction()
766-
return self.collection.update_many(
767-
criteria, pipeline, session=self.connection.session
768-
).matched_count
762+
return self.collection.update_many(criteria, pipeline).matched_count
769763

770764
def check_query(self):
771765
super().check_query()

django_mongodb_backend/features.py

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ 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 (see
40+
# DatabaseFeatures._supports_transactions), django.db.transaction.atomic() is a
41+
# no-op on this backend.
42+
supports_transactions = False
3943
supports_unspecified_pk = True
4044
uses_savepoints = False
4145

@@ -92,47 +96,13 @@ class DatabaseFeatures(BaseDatabaseFeatures):
9296
"expressions.tests.ExpressionOperatorTests.test_lefthand_bitwise_xor_right_null",
9397
"expressions.tests.ExpressionOperatorTests.test_lefthand_transformed_field_bitwise_or",
9498
}
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-
}
12599

126100
@cached_property
127101
def django_test_expected_failures(self):
128102
expected_failures = super().django_test_expected_failures
129103
expected_failures.update(self._django_test_expected_failures)
130104
if not self.is_mongodb_6_3:
131105
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)
136106
return expected_failures
137107

138108
django_test_skips = {
@@ -515,6 +485,17 @@ def django_test_expected_failures(self):
515485
"Connection health checks not implemented.": {
516486
"backends.base.test_base.ConnectionHealthChecksTests",
517487
},
488+
"transaction.atomic() is not supported.": {
489+
"backends.base.test_base.DatabaseWrapperLoggingTests",
490+
"basic.tests.SelectOnSaveTests.test_select_on_save_lying_update",
491+
"migrations.test_executor.ExecutorTests.test_atomic_operation_in_non_atomic_migration",
492+
"migrations.test_operations.OperationTests.test_run_python_atomic",
493+
},
494+
"transaction.rollback() is not supported.": {
495+
"transactions.tests.AtomicMiscTests.test_mark_for_rollback_on_error_in_autocommit",
496+
"transactions.tests.AtomicMiscTests.test_mark_for_rollback_on_error_in_transaction",
497+
"transactions.tests.NonAutocommitTests.test_orm_query_after_error_and_rollback",
498+
},
518499
"migrate --fake-initial is not supported.": {
519500
"migrations.test_commands.MigrateTests.test_migrate_fake_initial",
520501
"migrations.test_commands.MigrateTests.test_migrate_fake_split_initial",
@@ -609,15 +590,11 @@ def supports_atlas_search(self):
609590
return True
610591

611592
@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):
593+
def _supports_transactions(self):
618594
"""
619595
Transactions are enabled if MongoDB is configured as a replica set or a
620-
sharded cluster.
596+
sharded cluster. This is a MongoDB-specific feature flag distinct from Django's
597+
supports_transactions (without the underscore prefix).
621598
"""
622599
self.connection.ensure_connection()
623600
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 & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -41,47 +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-
:ref:`Deactivating transaction management <django:deactivate-transaction-management>`
62-
by setting :setting:`AUTOCOMMIT <DATABASE-AUTOCOMMIT>` to ``False`` in the
63-
:setting:`DATABASES` setting isn't supported.
64-
65-
.. _transactions-limitations:
66-
67-
Limitations
68-
-----------
69-
70-
MongoDB's transaction limitations that are applicable to Django are:
71-
72-
- :meth:`QuerySet.union() <django.db.models.query.QuerySet.union>` is not
73-
supported inside a transaction.
74-
- If a transaction raises an exception, the transaction is no longer usable.
75-
For example, if the update stage of :meth:`QuerySet.update_or_create()
76-
<django.db.models.query.QuerySet.update_or_create>` fails with
77-
:class:`~django.db.IntegrityError` due to a unique constraint violation, the
78-
create stage won't be able to proceed.
79-
:class:`pymongo.errors.OperationFailure` is raised, wrapped by
80-
:class:`django.db.DatabaseError`.
81-
- Savepoints (i.e. nested :func:`~django.db.transaction.atomic` blocks) aren't
82-
supported. The outermost :func:`~django.db.transaction.atomic` will start
83-
a transaction while any subsequent :func:`~django.db.transaction.atomic`
84-
blocks will have no effect.
85-
- Migration operations aren't :ref:`wrapped in a transaction
86-
<topics/migrations:transactions>` because of MongoDB restrictions such as
87-
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 & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import copy
2-
31
from django.core.exceptions import ImproperlyConfigured
42
from django.db import connection
53
from django.db.backends.signals import connection_created
6-
from django.test import SimpleTestCase, TransactionTestCase
4+
from django.test import SimpleTestCase, TestCase
75

86
from django_mongodb_backend.base import DatabaseWrapper
97

@@ -16,18 +14,8 @@ def test_database_name_empty(self):
1614
with self.assertRaisesMessage(ImproperlyConfigured, msg):
1715
DatabaseWrapper(settings).get_connection_params()
1816

19-
def test_autocommit_false(self):
20-
new_connection = connection.copy()
21-
new_connection.settings_dict = copy.deepcopy(connection.settings_dict)
22-
new_connection.settings_dict["AUTOCOMMIT"] = False
23-
msg = "MongoDB does not support AUTOCOMMIT=False."
24-
with self.assertRaisesMessage(ImproperlyConfigured, msg):
25-
new_connection.check_settings()
26-
27-
28-
class DatabaseWrapperConnectionTests(TransactionTestCase):
29-
available_apps = ["backend_"]
3017

18+
class DatabaseWrapperConnectionTests(TestCase):
3119
def test_set_autocommit(self):
3220
self.assertIs(connection.get_autocommit(), True)
3321
connection.set_autocommit(False)

0 commit comments

Comments
 (0)