Added a bulk option to RelatedManager remove() and clear() methods

Refs #21169
This commit is contained in:
Loic Bistuer 2013-11-22 00:14:16 +07:00 committed by Anssi Kääriäinen
parent 52015b963d
commit f450bc9f44
6 changed files with 90 additions and 37 deletions

View File

@ -8,7 +8,7 @@ from functools import partial
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import connection from django.db import connection
from django.db import models, router, DEFAULT_DB_ALIAS from django.db import models, router, transaction, DEFAULT_DB_ALIAS
from django.db.models import signals from django.db.models import signals
from django.db.models.fields.related import ForeignObject, ForeignObjectRel from django.db.models.fields.related import ForeignObject, ForeignObjectRel
from django.db.models.related import PathInfo from django.db.models.related import PathInfo
@ -382,16 +382,29 @@ def create_generic_related_manager(superclass):
obj.save() obj.save()
add.alters_data = True add.alters_data = True
def remove(self, *objs): def remove(self, *objs, **kwargs):
db = router.db_for_write(self.model, instance=self.instance) if not objs:
self.using(db).filter(pk__in=[o.pk for o in objs]).delete() return
bulk = kwargs.pop('bulk', True)
self._clear(self.filter(pk__in=[o.pk for o in objs]), bulk)
remove.alters_data = True remove.alters_data = True
def clear(self): def clear(self, **kwargs):
db = router.db_for_write(self.model, instance=self.instance) bulk = kwargs.pop('bulk', True)
self.using(db).delete() self._clear(self, bulk)
clear.alters_data = True clear.alters_data = True
def _clear(self, queryset, bulk):
db = router.db_for_write(self.model, instance=self.instance)
queryset = queryset.using(db)
if bulk:
queryset.delete()
else:
with transaction.commit_on_success_unless_managed(using=db, savepoint=False):
for obj in queryset:
obj.delete()
_clear.alters_data = True
def create(self, **kwargs): def create(self, **kwargs):
kwargs[self.content_type_field_name] = self.content_type kwargs[self.content_type_field_name] = self.content_type
kwargs[self.object_id_field_name] = self.pk_val kwargs[self.object_id_field_name] = self.pk_val

View File

@ -463,13 +463,11 @@ def create_foreign_related_manager(superclass, rel_field, rel_model):
# remove() and clear() are only provided if the ForeignKey can have a value of null. # remove() and clear() are only provided if the ForeignKey can have a value of null.
if rel_field.null: if rel_field.null:
def remove(self, *objs): def remove(self, *objs, **kwargs):
# If there aren't any objects, there is nothing to do.
if not objs: if not objs:
return return
bulk = kwargs.pop('bulk', True)
val = rel_field.get_foreign_related_value(self.instance) val = rel_field.get_foreign_related_value(self.instance)
old_ids = set() old_ids = set()
for obj in objs: for obj in objs:
# Is obj actually part of this descriptor set? # Is obj actually part of this descriptor set?
@ -477,14 +475,26 @@ def create_foreign_related_manager(superclass, rel_field, rel_model):
old_ids.add(obj.pk) old_ids.add(obj.pk)
else: else:
raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, self.instance)) raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, self.instance))
self._clear(self.filter(pk__in=old_ids), bulk)
self.filter(pk__in=old_ids).update(**{rel_field.name: None})
remove.alters_data = True remove.alters_data = True
def clear(self): def clear(self, **kwargs):
self.update(**{rel_field.name: None}) bulk = kwargs.pop('bulk', True)
self._clear(self, bulk)
clear.alters_data = True clear.alters_data = True
def _clear(self, queryset, bulk):
db = router.db_for_write(self.model, instance=self.instance)
queryset = queryset.using(db)
if bulk:
queryset.update(**{rel_field.name: None})
else:
with transaction.commit_on_success_unless_managed(using=db, savepoint=False):
for obj in queryset:
setattr(obj, rel_field.name, None)
obj.save()
_clear.alters_data = True
return RelatedManager return RelatedManager
@ -657,6 +667,7 @@ def create_many_related_manager(superclass, rel):
signals.m2m_changed.send(sender=self.through, action="pre_clear", signals.m2m_changed.send(sender=self.through, action="pre_clear",
instance=self.instance, reverse=self.reverse, instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=None, using=db) model=self.model, pk_set=None, using=db)
filters = self._build_remove_filters(super(ManyRelatedManager, self).get_queryset().using(db)) filters = self._build_remove_filters(super(ManyRelatedManager, self).get_queryset().using(db))
self.through._default_manager.using(db).filter(filters).delete() self.through._default_manager.using(db).filter(filters).delete()
@ -746,8 +757,6 @@ def create_many_related_manager(superclass, rel):
# 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
# target_field_name: the PK colname in join table for the target object # target_field_name: the PK colname in join table for the target object
# *objs - objects to remove # *objs - objects to remove
# If there aren't any objects, there is nothing to do.
if not objs: if not objs:
return return

View File

@ -1030,7 +1030,7 @@ class QuerySet(object):
""" """
Checks if this QuerySet has any filtering going on. Note that this Checks if this QuerySet has any filtering going on. Note that this
isn't equivalent for checking if all objects are present in results, isn't equivalent for checking if all objects are present in results,
for example qs[1:]._has_filters() -> True. for example qs[1:]._has_filters() -> False.
""" """
return self.query.has_filters() return self.query.has_filters()

View File

@ -110,6 +110,17 @@ Related objects reference
the ``blog`` :class:`~django.db.models.ForeignKey` doesn't have the ``blog`` :class:`~django.db.models.ForeignKey` doesn't have
``null=True``, this is invalid. ``null=True``, this is invalid.
.. versionchanged 1.7::
For :class:`~django.db.models.ForeignKey` objects, this method accepts
a ``bulk`` argument to control how to perform the operation.
If ``True`` (the default), ``QuerySet.update()`` is used.
If ``bulk=False``, the ``save()`` method of each individual model
instance is called instead. This triggers the
:data:`~django.db.models.signals.pre_save` and
:data:`~django.db.models.signals.post_save` signals and comes at the
expense of performance.
.. method:: clear() .. method:: clear()
Removes all objects from the related object set:: Removes all objects from the related object set::
@ -121,7 +132,8 @@ Related objects reference
them. them.
Just like ``remove()``, ``clear()`` is only available on Just like ``remove()``, ``clear()`` is only available on
:class:`~django.db.models.ForeignKey`\s where ``null=True``. :class:`~django.db.models.ForeignKey`\s where ``null=True`` and it also
accepts the ``bulk`` keyword argument.
.. note:: .. note::

View File

@ -430,6 +430,11 @@ Models
* :class:`F expressions <django.db.models.F>` support the power operator * :class:`F expressions <django.db.models.F>` support the power operator
(``**``). (``**``).
* The ``remove()`` and ``clear()`` methods of the related managers created by
``ForeignKey`` and ``GenericForeignKey`` now accept the ``bulk`` keyword
argument to control whether or not to perform operations in bulk
(i.e. using ``QuerySet.update()``). Defaults to ``True``.
Signals Signals
^^^^^^^ ^^^^^^^
@ -589,11 +594,13 @@ Fixing the issues introduced some backward incompatible changes:
- The default implementation of ``remove()`` for ``ForeignKey`` related managers - The default implementation of ``remove()`` for ``ForeignKey`` related managers
changed from a series of ``Model.save()`` calls to a single changed from a series of ``Model.save()`` calls to a single
``QuerySet.update()`` call. The change means that ``pre_save`` and ``QuerySet.update()`` call. The change means that ``pre_save`` and
``post_save`` signals aren't sent anymore. ``post_save`` signals aren't sent anymore. You can use the ``bulk=False``
keyword argument to revert to the previous behavior.
- The ``remove()`` and ``clear()`` methods for ``GenericForeignKey`` related - The ``remove()`` and ``clear()`` methods for ``GenericForeignKey`` related
managers now perform bulk delete. The ``Model.delete()`` method isn't called managers now perform bulk delete. The ``Model.delete()`` method isn't called
on each instance anymore. on each instance anymore. You can use the ``bulk=False`` keyword argument to
revert to the previous behavior.
- The ``remove()`` and ``clear()`` methods for ``ManyToManyField`` related - The ``remove()`` and ``clear()`` methods for ``ManyToManyField`` related
managers perform nested queries when filtering is involved, which may or managers perform nested queries when filtering is involved, which may or

View File

@ -226,11 +226,11 @@ class CustomManagerTests(TestCase):
ordered=False, ordered=False,
) )
def test_removal_through_default_fk_related_manager(self): def test_removal_through_default_fk_related_manager(self, bulk=True):
bugs = FunPerson.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_book=self.b1) bugs = FunPerson.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_book=self.b1)
droopy = FunPerson.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_book=self.b1) droopy = FunPerson.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_book=self.b1)
self.b1.fun_people_favorite_books.remove(droopy) self.b1.fun_people_favorite_books.remove(droopy, bulk=bulk)
self.assertQuerysetEqual( self.assertQuerysetEqual(
FunPerson._base_manager.filter(favorite_book=self.b1), [ FunPerson._base_manager.filter(favorite_book=self.b1), [
"Bugs", "Bugs",
@ -240,7 +240,7 @@ class CustomManagerTests(TestCase):
ordered=False, ordered=False,
) )
self.b1.fun_people_favorite_books.remove(bugs) self.b1.fun_people_favorite_books.remove(bugs, bulk=bulk)
self.assertQuerysetEqual( self.assertQuerysetEqual(
FunPerson._base_manager.filter(favorite_book=self.b1), [ FunPerson._base_manager.filter(favorite_book=self.b1), [
"Droopy", "Droopy",
@ -251,7 +251,7 @@ class CustomManagerTests(TestCase):
bugs.favorite_book = self.b1 bugs.favorite_book = self.b1
bugs.save() bugs.save()
self.b1.fun_people_favorite_books.clear() self.b1.fun_people_favorite_books.clear(bulk=bulk)
self.assertQuerysetEqual( self.assertQuerysetEqual(
FunPerson._base_manager.filter(favorite_book=self.b1), [ FunPerson._base_manager.filter(favorite_book=self.b1), [
"Droopy", "Droopy",
@ -260,12 +260,15 @@ class CustomManagerTests(TestCase):
ordered=False, ordered=False,
) )
def test_removal_through_specified_fk_related_manager(self): def test_slow_removal_through_default_fk_related_manager(self):
self.test_removal_through_default_fk_related_manager(bulk=False)
def test_removal_through_specified_fk_related_manager(self, bulk=True):
Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_book=self.b1) Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_book=self.b1)
droopy = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_book=self.b1) droopy = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_book=self.b1)
# Check that the fun manager DOESN'T remove boring people. # Check that the fun manager DOESN'T remove boring people.
self.b1.favorite_books(manager='fun_people').remove(droopy) self.b1.favorite_books(manager='fun_people').remove(droopy, bulk=bulk)
self.assertQuerysetEqual( self.assertQuerysetEqual(
self.b1.favorite_books(manager='boring_people').all(), [ self.b1.favorite_books(manager='boring_people').all(), [
"Droopy", "Droopy",
@ -274,7 +277,7 @@ class CustomManagerTests(TestCase):
ordered=False, ordered=False,
) )
# Check that the boring manager DOES remove boring people. # Check that the boring manager DOES remove boring people.
self.b1.favorite_books(manager='boring_people').remove(droopy) self.b1.favorite_books(manager='boring_people').remove(droopy, bulk=bulk)
self.assertQuerysetEqual( self.assertQuerysetEqual(
self.b1.favorite_books(manager='boring_people').all(), [ self.b1.favorite_books(manager='boring_people').all(), [
], ],
@ -285,7 +288,7 @@ class CustomManagerTests(TestCase):
droopy.save() droopy.save()
# Check that the fun manager ONLY clears fun people. # Check that the fun manager ONLY clears fun people.
self.b1.favorite_books(manager='fun_people').clear() self.b1.favorite_books(manager='fun_people').clear(bulk=bulk)
self.assertQuerysetEqual( self.assertQuerysetEqual(
self.b1.favorite_books(manager='boring_people').all(), [ self.b1.favorite_books(manager='boring_people').all(), [
"Droopy", "Droopy",
@ -300,11 +303,14 @@ class CustomManagerTests(TestCase):
ordered=False, ordered=False,
) )
def test_removal_through_default_gfk_related_manager(self): def test_slow_removal_through_specified_fk_related_manager(self):
self.test_removal_through_specified_fk_related_manager(bulk=False)
def test_removal_through_default_gfk_related_manager(self, bulk=True):
bugs = FunPerson.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_thing=self.b1) bugs = FunPerson.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_thing=self.b1)
droopy = FunPerson.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_thing=self.b1) droopy = FunPerson.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_thing=self.b1)
self.b1.fun_people_favorite_things.remove(droopy) self.b1.fun_people_favorite_things.remove(droopy, bulk=bulk)
self.assertQuerysetEqual( self.assertQuerysetEqual(
FunPerson._base_manager.order_by('first_name').filter(favorite_thing_id=self.b1.pk), [ FunPerson._base_manager.order_by('first_name').filter(favorite_thing_id=self.b1.pk), [
"Bugs", "Bugs",
@ -314,7 +320,7 @@ class CustomManagerTests(TestCase):
ordered=False, ordered=False,
) )
self.b1.fun_people_favorite_things.remove(bugs) self.b1.fun_people_favorite_things.remove(bugs, bulk=bulk)
self.assertQuerysetEqual( self.assertQuerysetEqual(
FunPerson._base_manager.order_by('first_name').filter(favorite_thing_id=self.b1.pk), [ FunPerson._base_manager.order_by('first_name').filter(favorite_thing_id=self.b1.pk), [
"Droopy", "Droopy",
@ -325,7 +331,7 @@ class CustomManagerTests(TestCase):
bugs.favorite_book = self.b1 bugs.favorite_book = self.b1
bugs.save() bugs.save()
self.b1.fun_people_favorite_things.clear() self.b1.fun_people_favorite_things.clear(bulk=bulk)
self.assertQuerysetEqual( self.assertQuerysetEqual(
FunPerson._base_manager.order_by('first_name').filter(favorite_thing_id=self.b1.pk), [ FunPerson._base_manager.order_by('first_name').filter(favorite_thing_id=self.b1.pk), [
"Droopy", "Droopy",
@ -334,12 +340,15 @@ class CustomManagerTests(TestCase):
ordered=False, ordered=False,
) )
def test_removal_through_specified_gfk_related_manager(self): def test_slow_removal_through_default_gfk_related_manager(self):
self.test_removal_through_default_gfk_related_manager(bulk=False)
def test_removal_through_specified_gfk_related_manager(self, bulk=True):
Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_thing=self.b1) Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_thing=self.b1)
droopy = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_thing=self.b1) droopy = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_thing=self.b1)
# Check that the fun manager DOESN'T remove boring people. # Check that the fun manager DOESN'T remove boring people.
self.b1.favorite_things(manager='fun_people').remove(droopy) self.b1.favorite_things(manager='fun_people').remove(droopy, bulk=bulk)
self.assertQuerysetEqual( self.assertQuerysetEqual(
self.b1.favorite_things(manager='boring_people').all(), [ self.b1.favorite_things(manager='boring_people').all(), [
"Droopy", "Droopy",
@ -349,7 +358,7 @@ class CustomManagerTests(TestCase):
) )
# Check that the boring manager DOES remove boring people. # Check that the boring manager DOES remove boring people.
self.b1.favorite_things(manager='boring_people').remove(droopy) self.b1.favorite_things(manager='boring_people').remove(droopy, bulk=bulk)
self.assertQuerysetEqual( self.assertQuerysetEqual(
self.b1.favorite_things(manager='boring_people').all(), [ self.b1.favorite_things(manager='boring_people').all(), [
], ],
@ -360,7 +369,7 @@ class CustomManagerTests(TestCase):
droopy.save() droopy.save()
# Check that the fun manager ONLY clears fun people. # Check that the fun manager ONLY clears fun people.
self.b1.favorite_things(manager='fun_people').clear() self.b1.favorite_things(manager='fun_people').clear(bulk=bulk)
self.assertQuerysetEqual( self.assertQuerysetEqual(
self.b1.favorite_things(manager='boring_people').all(), [ self.b1.favorite_things(manager='boring_people').all(), [
"Droopy", "Droopy",
@ -375,6 +384,9 @@ class CustomManagerTests(TestCase):
ordered=False, ordered=False,
) )
def test_slow_removal_through_specified_gfk_related_manager(self):
self.test_removal_through_specified_gfk_related_manager(bulk=False)
def test_removal_through_default_m2m_related_manager(self): def test_removal_through_default_m2m_related_manager(self):
bugs = FunPerson.objects.create(first_name="Bugs", last_name="Bunny", fun=True) bugs = FunPerson.objects.create(first_name="Bugs", last_name="Bunny", fun=True)
self.b1.fun_authors.add(bugs) self.b1.fun_authors.add(bugs)