mirror of
https://github.com/django/django.git
synced 2025-01-22 00:02:15 +00:00
Fixed transaction handling for a number of operations on related objects.
Thanks Anssi and Aymeric for the reviews. Refs #21174.
This commit is contained in:
parent
975337e5c3
commit
bc9be72bdc
@ -394,9 +394,12 @@ class ReverseGenericRelatedObjectsDescriptor(object):
|
|||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
manager = self.__get__(instance)
|
manager = self.__get__(instance)
|
||||||
manager.clear()
|
|
||||||
for obj in value:
|
db = router.db_for_write(manager.model, instance=manager.instance)
|
||||||
manager.add(obj)
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
|
manager.clear()
|
||||||
|
for obj in value:
|
||||||
|
manager.add(obj)
|
||||||
|
|
||||||
|
|
||||||
def create_generic_related_manager(superclass):
|
def create_generic_related_manager(superclass):
|
||||||
@ -474,12 +477,14 @@ def create_generic_related_manager(superclass):
|
|||||||
self.prefetch_cache_name)
|
self.prefetch_cache_name)
|
||||||
|
|
||||||
def add(self, *objs):
|
def add(self, *objs):
|
||||||
for obj in objs:
|
db = router.db_for_write(self.model, instance=self.instance)
|
||||||
if not isinstance(obj, self.model):
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
|
for obj in objs:
|
||||||
setattr(obj, self.content_type_field_name, self.content_type)
|
if not isinstance(obj, self.model):
|
||||||
setattr(obj, self.object_id_field_name, self.pk_val)
|
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
|
||||||
obj.save()
|
setattr(obj, self.content_type_field_name, self.content_type)
|
||||||
|
setattr(obj, self.object_id_field_name, self.pk_val)
|
||||||
|
obj.save()
|
||||||
add.alters_data = True
|
add.alters_data = True
|
||||||
|
|
||||||
def remove(self, *objs, **kwargs):
|
def remove(self, *objs, **kwargs):
|
||||||
@ -498,6 +503,8 @@ def create_generic_related_manager(superclass):
|
|||||||
db = router.db_for_write(self.model, instance=self.instance)
|
db = router.db_for_write(self.model, instance=self.instance)
|
||||||
queryset = queryset.using(db)
|
queryset = queryset.using(db)
|
||||||
if bulk:
|
if bulk:
|
||||||
|
# `QuerySet.delete()` creates its own atomic block which
|
||||||
|
# contains the `pre_delete` and `post_delete` signal handlers.
|
||||||
queryset.delete()
|
queryset.delete()
|
||||||
else:
|
else:
|
||||||
with transaction.atomic(using=db, savepoint=False):
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
|
@ -735,6 +735,7 @@ def create_foreign_related_manager(superclass, rel_field, rel_model):
|
|||||||
db = router.db_for_write(self.model, instance=self.instance)
|
db = router.db_for_write(self.model, instance=self.instance)
|
||||||
queryset = queryset.using(db)
|
queryset = queryset.using(db)
|
||||||
if bulk:
|
if bulk:
|
||||||
|
# `QuerySet.update()` is intrinsically atomic.
|
||||||
queryset.update(**{rel_field.name: None})
|
queryset.update(**{rel_field.name: None})
|
||||||
else:
|
else:
|
||||||
with transaction.atomic(using=db, savepoint=False):
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
@ -763,11 +764,14 @@ class ForeignRelatedObjectsDescriptor(object):
|
|||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
manager = self.__get__(instance)
|
manager = self.__get__(instance)
|
||||||
# If the foreign key can support nulls, then completely clear the related set.
|
|
||||||
# Otherwise, just move the named objects into the set.
|
db = router.db_for_write(manager.model, instance=manager.instance)
|
||||||
if self.related.field.null:
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
manager.clear()
|
# If the foreign key can support nulls, then completely clear the related set.
|
||||||
manager.add(*value)
|
# Otherwise, just move the named objects into the set.
|
||||||
|
if self.related.field.null:
|
||||||
|
manager.clear()
|
||||||
|
manager.add(*value)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def related_manager_cls(self):
|
def related_manager_cls(self):
|
||||||
@ -901,11 +905,14 @@ def create_many_related_manager(superclass, rel):
|
|||||||
"Cannot use add() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." %
|
"Cannot use add() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." %
|
||||||
(opts.app_label, opts.object_name)
|
(opts.app_label, opts.object_name)
|
||||||
)
|
)
|
||||||
self._add_items(self.source_field_name, self.target_field_name, *objs)
|
|
||||||
|
|
||||||
# If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
|
db = router.db_for_write(self.through, instance=self.instance)
|
||||||
if self.symmetrical:
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
self._add_items(self.target_field_name, self.source_field_name, *objs)
|
self._add_items(self.source_field_name, self.target_field_name, *objs)
|
||||||
|
|
||||||
|
# If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
|
||||||
|
if self.symmetrical:
|
||||||
|
self._add_items(self.target_field_name, self.source_field_name, *objs)
|
||||||
add.alters_data = True
|
add.alters_data = True
|
||||||
|
|
||||||
def remove(self, *objs):
|
def remove(self, *objs):
|
||||||
@ -920,17 +927,17 @@ def create_many_related_manager(superclass, rel):
|
|||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
db = router.db_for_write(self.through, instance=self.instance)
|
db = router.db_for_write(self.through, instance=self.instance)
|
||||||
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
|
signals.m2m_changed.send(sender=self.through, action="pre_clear",
|
||||||
|
instance=self.instance, reverse=self.reverse,
|
||||||
|
model=self.model, pk_set=None, using=db)
|
||||||
|
|
||||||
signals.m2m_changed.send(sender=self.through, action="pre_clear",
|
filters = self._build_remove_filters(super(ManyRelatedManager, self).get_queryset().using(db))
|
||||||
instance=self.instance, reverse=self.reverse,
|
self.through._default_manager.using(db).filter(filters).delete()
|
||||||
model=self.model, pk_set=None, using=db)
|
|
||||||
|
|
||||||
filters = self._build_remove_filters(super(ManyRelatedManager, self).get_queryset().using(db))
|
signals.m2m_changed.send(sender=self.through, action="post_clear",
|
||||||
self.through._default_manager.using(db).filter(filters).delete()
|
instance=self.instance, reverse=self.reverse,
|
||||||
|
model=self.model, pk_set=None, using=db)
|
||||||
signals.m2m_changed.send(sender=self.through, action="post_clear",
|
|
||||||
instance=self.instance, reverse=self.reverse,
|
|
||||||
model=self.model, pk_set=None, using=db)
|
|
||||||
clear.alters_data = True
|
clear.alters_data = True
|
||||||
|
|
||||||
def create(self, **kwargs):
|
def create(self, **kwargs):
|
||||||
@ -990,35 +997,39 @@ def create_many_related_manager(superclass, rel):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
new_ids.add(obj)
|
new_ids.add(obj)
|
||||||
|
|
||||||
db = router.db_for_write(self.through, instance=self.instance)
|
db = router.db_for_write(self.through, instance=self.instance)
|
||||||
vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True)
|
vals = (self.through._default_manager.using(db)
|
||||||
vals = vals.filter(**{
|
.values_list(target_field_name, flat=True)
|
||||||
source_field_name: self.related_val[0],
|
.filter(**{
|
||||||
'%s__in' % target_field_name: new_ids,
|
source_field_name: self.related_val[0],
|
||||||
})
|
'%s__in' % target_field_name: new_ids,
|
||||||
|
}))
|
||||||
new_ids = new_ids - set(vals)
|
new_ids = new_ids - set(vals)
|
||||||
|
|
||||||
if self.reverse or source_field_name == self.source_field_name:
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
# Don't send the signal when we are inserting the
|
if self.reverse or source_field_name == self.source_field_name:
|
||||||
# duplicate data row for symmetrical reverse entries.
|
# Don't send the signal when we are inserting the
|
||||||
signals.m2m_changed.send(sender=self.through, action='pre_add',
|
# duplicate data row for symmetrical reverse entries.
|
||||||
instance=self.instance, reverse=self.reverse,
|
signals.m2m_changed.send(sender=self.through, action='pre_add',
|
||||||
model=self.model, pk_set=new_ids, using=db)
|
instance=self.instance, reverse=self.reverse,
|
||||||
# Add the ones that aren't there already
|
model=self.model, pk_set=new_ids, using=db)
|
||||||
self.through._default_manager.using(db).bulk_create([
|
|
||||||
self.through(**{
|
|
||||||
'%s_id' % source_field_name: self.related_val[0],
|
|
||||||
'%s_id' % target_field_name: obj_id,
|
|
||||||
})
|
|
||||||
for obj_id in new_ids
|
|
||||||
])
|
|
||||||
|
|
||||||
if self.reverse or source_field_name == self.source_field_name:
|
# Add the ones that aren't there already
|
||||||
# Don't send the signal when we are inserting the
|
self.through._default_manager.using(db).bulk_create([
|
||||||
# duplicate data row for symmetrical reverse entries.
|
self.through(**{
|
||||||
signals.m2m_changed.send(sender=self.through, action='post_add',
|
'%s_id' % source_field_name: self.related_val[0],
|
||||||
instance=self.instance, reverse=self.reverse,
|
'%s_id' % target_field_name: obj_id,
|
||||||
model=self.model, pk_set=new_ids, using=db)
|
})
|
||||||
|
for obj_id in new_ids
|
||||||
|
])
|
||||||
|
|
||||||
|
if self.reverse or source_field_name == self.source_field_name:
|
||||||
|
# Don't send the signal when we are inserting the
|
||||||
|
# duplicate data row for symmetrical reverse entries.
|
||||||
|
signals.m2m_changed.send(sender=self.through, action='post_add',
|
||||||
|
instance=self.instance, reverse=self.reverse,
|
||||||
|
model=self.model, pk_set=new_ids, using=db)
|
||||||
|
|
||||||
def _remove_items(self, source_field_name, target_field_name, *objs):
|
def _remove_items(self, source_field_name, target_field_name, *objs):
|
||||||
# source_field_name: the PK colname in join table for the source object
|
# source_field_name: the PK colname in join table for the source object
|
||||||
@ -1037,23 +1048,23 @@ def create_many_related_manager(superclass, rel):
|
|||||||
old_ids.add(obj)
|
old_ids.add(obj)
|
||||||
|
|
||||||
db = router.db_for_write(self.through, instance=self.instance)
|
db = router.db_for_write(self.through, instance=self.instance)
|
||||||
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
|
# Send a signal to the other end if need be.
|
||||||
|
signals.m2m_changed.send(sender=self.through, action="pre_remove",
|
||||||
|
instance=self.instance, reverse=self.reverse,
|
||||||
|
model=self.model, pk_set=old_ids, using=db)
|
||||||
|
target_model_qs = super(ManyRelatedManager, self).get_queryset()
|
||||||
|
if target_model_qs._has_filters():
|
||||||
|
old_vals = target_model_qs.using(db).filter(**{
|
||||||
|
'%s__in' % self.target_field.related_field.attname: old_ids})
|
||||||
|
else:
|
||||||
|
old_vals = old_ids
|
||||||
|
filters = self._build_remove_filters(old_vals)
|
||||||
|
self.through._default_manager.using(db).filter(filters).delete()
|
||||||
|
|
||||||
# Send a signal to the other end if need be.
|
signals.m2m_changed.send(sender=self.through, action="post_remove",
|
||||||
signals.m2m_changed.send(sender=self.through, action="pre_remove",
|
instance=self.instance, reverse=self.reverse,
|
||||||
instance=self.instance, reverse=self.reverse,
|
model=self.model, pk_set=old_ids, using=db)
|
||||||
model=self.model, pk_set=old_ids, using=db)
|
|
||||||
target_model_qs = super(ManyRelatedManager, self).get_queryset()
|
|
||||||
if target_model_qs._has_filters():
|
|
||||||
old_vals = target_model_qs.using(db).filter(**{
|
|
||||||
'%s__in' % self.target_field.related_field.attname: old_ids})
|
|
||||||
else:
|
|
||||||
old_vals = old_ids
|
|
||||||
filters = self._build_remove_filters(old_vals)
|
|
||||||
self.through._default_manager.using(db).filter(filters).delete()
|
|
||||||
|
|
||||||
signals.m2m_changed.send(sender=self.through, action="post_remove",
|
|
||||||
instance=self.instance, reverse=self.reverse,
|
|
||||||
model=self.model, pk_set=old_ids, using=db)
|
|
||||||
|
|
||||||
return ManyRelatedManager
|
return ManyRelatedManager
|
||||||
|
|
||||||
@ -1103,8 +1114,11 @@ class ManyRelatedObjectsDescriptor(object):
|
|||||||
raise AttributeError("Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))
|
raise AttributeError("Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))
|
||||||
|
|
||||||
manager = self.__get__(instance)
|
manager = self.__get__(instance)
|
||||||
manager.clear()
|
|
||||||
manager.add(*value)
|
db = router.db_for_write(manager.through, instance=manager.instance)
|
||||||
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
|
manager.clear()
|
||||||
|
manager.add(*value)
|
||||||
|
|
||||||
|
|
||||||
class ReverseManyRelatedObjectsDescriptor(object):
|
class ReverseManyRelatedObjectsDescriptor(object):
|
||||||
@ -1157,11 +1171,15 @@ class ReverseManyRelatedObjectsDescriptor(object):
|
|||||||
raise AttributeError("Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))
|
raise AttributeError("Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))
|
||||||
|
|
||||||
manager = self.__get__(instance)
|
manager = self.__get__(instance)
|
||||||
|
|
||||||
# clear() can change expected output of 'value' queryset, we force evaluation
|
# clear() can change expected output of 'value' queryset, we force evaluation
|
||||||
# of queryset before clear; ticket #19816
|
# of queryset before clear; ticket #19816
|
||||||
value = tuple(value)
|
value = tuple(value)
|
||||||
manager.clear()
|
|
||||||
manager.add(*value)
|
db = router.db_for_write(manager.through, instance=manager.instance)
|
||||||
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
|
manager.clear()
|
||||||
|
manager.add(*value)
|
||||||
|
|
||||||
|
|
||||||
class ForeignObjectRel(object):
|
class ForeignObjectRel(object):
|
||||||
|
@ -168,7 +168,19 @@ Backwards incompatible changes in 1.8
|
|||||||
deprecation timeline for a given feature, its removal may appear as a
|
deprecation timeline for a given feature, its removal may appear as a
|
||||||
backwards incompatible change.
|
backwards incompatible change.
|
||||||
|
|
||||||
...
|
* Some operations on related objects such as
|
||||||
|
:meth:`~django.db.models.fields.related.RelatedManager.add()` or
|
||||||
|
:ref:`direct assignment<direct-assignment>` ran multiple data modifying
|
||||||
|
queries without wrapping them in transactions. To reduce the risk of data
|
||||||
|
corruption, all data modifying methods that affect multiple related objects
|
||||||
|
(i.e. ``add()``, ``remove()``, ``clear()``, and
|
||||||
|
:ref:`direct assignment<direct-assignment>`) now perform their data modifying
|
||||||
|
queries from within a transaction, provided your database supports
|
||||||
|
transactions.
|
||||||
|
|
||||||
|
This has one backwards incompatible side effect, signal handlers triggered
|
||||||
|
from these methods are now executed within the method's transaction and
|
||||||
|
any exception in a signal handler will prevent the whole operation.
|
||||||
|
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
@ -54,7 +55,9 @@ class ManyToManyTests(TestCase):
|
|||||||
|
|
||||||
# Adding an object of the wrong type raises TypeError
|
# Adding an object of the wrong type raises TypeError
|
||||||
with six.assertRaisesRegex(self, TypeError, "'Publication' instance expected, got <Article.*"):
|
with six.assertRaisesRegex(self, TypeError, "'Publication' instance expected, got <Article.*"):
|
||||||
a6.publications.add(a5)
|
with transaction.atomic():
|
||||||
|
a6.publications.add(a5)
|
||||||
|
|
||||||
# Add a Publication directly via publications.add by using keyword arguments.
|
# Add a Publication directly via publications.add by using keyword arguments.
|
||||||
a6.publications.create(title='Highlights for Adults')
|
a6.publications.create(title='Highlights for Adults')
|
||||||
self.assertQuerysetEqual(a6.publications.all(),
|
self.assertQuerysetEqual(a6.publications.all(),
|
||||||
|
@ -295,19 +295,23 @@ class QueryTestCase(TestCase):
|
|||||||
|
|
||||||
# Add to an m2m with an object from a different database
|
# Add to an m2m with an object from a different database
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
marty.book_set.add(dive)
|
with transaction.atomic(using='default'):
|
||||||
|
marty.book_set.add(dive)
|
||||||
|
|
||||||
# Set a m2m with an object from a different database
|
# Set a m2m with an object from a different database
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
marty.book_set = [pro, dive]
|
with transaction.atomic(using='default'):
|
||||||
|
marty.book_set = [pro, dive]
|
||||||
|
|
||||||
# Add to a reverse m2m with an object from a different database
|
# Add to a reverse m2m with an object from a different database
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
dive.authors.add(marty)
|
with transaction.atomic(using='other'):
|
||||||
|
dive.authors.add(marty)
|
||||||
|
|
||||||
# Set a reverse m2m with an object from a different database
|
# Set a reverse m2m with an object from a different database
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
dive.authors = [mark, marty]
|
with transaction.atomic(using='other'):
|
||||||
|
dive.authors = [mark, marty]
|
||||||
|
|
||||||
def test_m2m_deletion(self):
|
def test_m2m_deletion(self):
|
||||||
"Cascaded deletions of m2m relations issue queries on the right database"
|
"Cascaded deletions of m2m relations issue queries on the right database"
|
||||||
@ -762,7 +766,8 @@ class QueryTestCase(TestCase):
|
|||||||
|
|
||||||
# Add to a foreign key set with an object from a different database
|
# Add to a foreign key set with an object from a different database
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
dive.reviews.add(review1)
|
with transaction.atomic(using='other'):
|
||||||
|
dive.reviews.add(review1)
|
||||||
|
|
||||||
# BUT! if you assign a FK object when the base object hasn't
|
# BUT! if you assign a FK object when the base object hasn't
|
||||||
# been saved yet, you implicitly assign the database for the
|
# been saved yet, you implicitly assign the database for the
|
||||||
|
Loading…
x
Reference in New Issue
Block a user