Commit adc0c4fb authored by Tim Graham's avatar Tim Graham Committed by Loïc Bistuer
Browse files

Fixed #18556 -- Allowed RelatedManager.add() to execute 1 query where possible.

Thanks Loic Bistuer for review.
parent c2e70f02
Loading
Loading
Loading
Loading
+33 −9
Original line number Diff line number Diff line
@@ -501,14 +501,37 @@ def create_generic_related_manager(superclass, rel):
                    False,
                    self.prefetch_cache_name)

        def add(self, *objs):
        def add(self, *objs, **kwargs):
            bulk = kwargs.pop('bulk', True)
            db = router.db_for_write(self.model, instance=self.instance)
            with transaction.atomic(using=db, savepoint=False):
                for obj in objs:

            def check_and_update_obj(obj):
                if not isinstance(obj, self.model):
                        raise TypeError("'%s' instance expected" % self.model._meta.object_name)
                    raise TypeError("'%s' instance expected, got %r" % (
                        self.model._meta.object_name, obj
                    ))
                setattr(obj, self.content_type_field_name, self.content_type)
                setattr(obj, self.object_id_field_name, self.pk_val)

            if bulk:
                pks = []
                for obj in objs:
                    if obj._state.adding or obj._state.db != db:
                        raise ValueError(
                            "%r instance isn't saved. Use bulk=False or save "
                            "the object first. but must be." % obj
                        )
                    check_and_update_obj(obj)
                    pks.append(obj.pk)

                self.model._base_manager.using(db).filter(pk__in=pks).update(**{
                    self.content_type_field_name: self.content_type,
                    self.object_id_field_name: self.pk_val,
                })
            else:
                with transaction.atomic(using=db, savepoint=False):
                    for obj in objs:
                        check_and_update_obj(obj)
                        obj.save()
        add.alters_data = True

@@ -542,13 +565,14 @@ def create_generic_related_manager(superclass, rel):
            # could be affected by `manager.clear()`. Refs #19816.
            objs = tuple(objs)

            bulk = kwargs.pop('bulk', True)
            clear = kwargs.pop('clear', False)

            db = router.db_for_write(self.model, instance=self.instance)
            with transaction.atomic(using=db, savepoint=False):
                if clear:
                    self.clear()
                    self.add(*objs)
                    self.add(*objs, bulk=bulk)
                else:
                    old_objs = set(self.using(db).all())
                    new_objs = []
@@ -559,7 +583,7 @@ def create_generic_related_manager(superclass, rel):
                            new_objs.append(obj)

                    self.remove(*old_objs)
                    self.add(*new_objs)
                    self.add(*new_objs, bulk=bulk)
        set.alters_data = True

        def create(self, **kwargs):
+32 −11
Original line number Diff line number Diff line
@@ -765,15 +765,35 @@ def create_foreign_related_manager(superclass, rel):
            cache_name = self.field.related_query_name()
            return queryset, rel_obj_attr, instance_attr, False, cache_name

        def add(self, *objs):
        def add(self, *objs, **kwargs):
            bulk = kwargs.pop('bulk', True)
            objs = list(objs)
            db = router.db_for_write(self.model, instance=self.instance)
            with transaction.atomic(using=db, savepoint=False):
                for obj in objs:

            def check_and_update_obj(obj):
                if not isinstance(obj, self.model):
                        raise TypeError("'%s' instance expected, got %r" %
                                        (self.model._meta.object_name, obj))
                    raise TypeError("'%s' instance expected, got %r" % (
                        self.model._meta.object_name, obj,
                    ))
                setattr(obj, self.field.name, self.instance)

            if bulk:
                pks = []
                for obj in objs:
                    check_and_update_obj(obj)
                    if obj._state.adding or obj._state.db != db:
                        raise ValueError(
                            "%r instance isn't saved. Use bulk=False or save "
                            "the object first." % obj
                        )
                    pks.append(obj.pk)
                self.model._base_manager.using(db).filter(pk__in=pks).update(**{
                    self.field.name: self.instance,
                })
            else:
                with transaction.atomic(using=db, savepoint=False):
                    for obj in objs:
                        check_and_update_obj(obj)
                        obj.save()
        add.alters_data = True

@@ -835,6 +855,7 @@ def create_foreign_related_manager(superclass, rel):
            # could be affected by `manager.clear()`. Refs #19816.
            objs = tuple(objs)

            bulk = kwargs.pop('bulk', True)
            clear = kwargs.pop('clear', False)

            if self.field.null:
@@ -842,7 +863,7 @@ def create_foreign_related_manager(superclass, rel):
                with transaction.atomic(using=db, savepoint=False):
                    if clear:
                        self.clear()
                        self.add(*objs)
                        self.add(*objs, bulk=bulk)
                    else:
                        old_objs = set(self.using(db).all())
                        new_objs = []
@@ -852,10 +873,10 @@ def create_foreign_related_manager(superclass, rel):
                            else:
                                new_objs.append(obj)

                        self.remove(*old_objs)
                        self.add(*new_objs)
                        self.remove(*old_objs, bulk=bulk)
                        self.add(*new_objs, bulk=bulk)
            else:
                self.add(*objs)
                self.add(*objs, bulk=bulk)
        set.alters_data = True

    return RelatedManager
+17 −3
Original line number Diff line number Diff line
@@ -36,7 +36,7 @@ Related objects reference
      In this example, the methods below will be available both on
      ``topping.pizza_set`` and on ``pizza.toppings``.

    .. method:: add(*objs)
    .. method:: add(*objs, bulk=True)

        Adds the specified model objects to the related object set.

@@ -48,7 +48,13 @@ Related objects reference

        In the example above, in the case of a
        :class:`~django.db.models.ForeignKey` relationship,
        ``e.save()`` is called by the related manager to perform the update.
        :meth:`QuerySet.update() <django.db.models.query.QuerySet.update>`
        is used to perform the update. This requires the objects to already be
        saved.

        You can use the ``bulk=False`` argument to instead have the related
        manager perform the update by calling ``e.save()``.

        Using ``add()`` with a many-to-many relationship, however, will not
        call any ``save()`` methods, but rather create the relationships
        using :meth:`QuerySet.bulk_create()
@@ -56,6 +62,12 @@ Related objects reference
        some custom logic when a relationship is created, listen to the
        :data:`~django.db.models.signals.m2m_changed` signal.

        .. versionchanged:: 1.9

            The ``bulk`` parameter was added. In order versions, foreign key
            updates were always done using ``save()``. Use ``bulk=False`` if
            you require the old behavior.

    .. method:: create(**kwargs)

        Creates a new object, saves it and puts it in the related object set.
@@ -135,7 +147,7 @@ Related objects reference
        :class:`~django.db.models.ForeignKey`\s where ``null=True`` and it also
        accepts the ``bulk`` keyword argument.

    .. method:: set(objs, clear=False)
    .. method:: set(objs, bulk=True, clear=False)

        .. versionadded:: 1.9

@@ -150,6 +162,8 @@ Related objects reference
        If ``clear=True``, the ``clear()`` method is called instead and the
        whole set is added at once.

        The ``bulk`` argument is passed on to :meth:`add`.

        Note that since ``set()`` is a compound operation, it is subject to
        race conditions. For instance, new objects may be added to the database
        in between the call to ``clear()`` and the call to ``add()``.
+14 −0
Original line number Diff line number Diff line
@@ -400,6 +400,11 @@ Models
  managers created by ``ForeignKey``, ``GenericForeignKey``, and
  ``ManyToManyField``.

* The :meth:`~django.db.models.fields.related.RelatedManager.add` method on
  a reverse foreign key now has a ``bulk`` parameter to allow executing one
  query regardless of the number of objects being added rather than one query
  per object.

* Added the ``keep_parents`` parameter to :meth:`Model.delete()
  <django.db.models.Model.delete>` to allow deleting only a child's data in a
  model that uses multi-table inheritance.
@@ -669,6 +674,15 @@ Dropped support for PostgreSQL 9.0
Upstream support for PostgreSQL 9.0 ended in September 2015. As a consequence,
Django 1.9 sets 9.1 as the minimum PostgreSQL version it officially supports.

Bulk behavior of ``add()`` method of related managers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To improve performance, the ``add()`` methods of the related managers created
by ``ForeignKey`` and ``GenericForeignKey`` changed from a series of
``Model.save()`` calls to a single ``QuerySet.update()`` call. The change means
that ``pre_save`` and ``post_save`` signals aren't sent anymore. You can use
the ``bulk=False`` keyword argument to revert to the previous behavior.

Template ``LoaderOrigin`` and ``StringOrigin`` are removed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+28 −2
Original line number Diff line number Diff line
@@ -247,6 +247,32 @@ class GenericRelationsTests(TestCase):
            self.comp_func
        )

    def test_add_bulk(self):
        bacon = Vegetable.objects.create(name="Bacon", is_yucky=False)
        t1 = TaggedItem.objects.create(content_object=self.quartz, tag="shiny")
        t2 = TaggedItem.objects.create(content_object=self.quartz, tag="clearish")
        # One update() query.
        with self.assertNumQueries(1):
            bacon.tags.add(t1, t2)
        self.assertEqual(t1.content_object, bacon)
        self.assertEqual(t2.content_object, bacon)

    def test_add_bulk_false(self):
        bacon = Vegetable.objects.create(name="Bacon", is_yucky=False)
        t1 = TaggedItem.objects.create(content_object=self.quartz, tag="shiny")
        t2 = TaggedItem.objects.create(content_object=self.quartz, tag="clearish")
        # One save() for each object.
        with self.assertNumQueries(2):
            bacon.tags.add(t1, t2, bulk=False)
        self.assertEqual(t1.content_object, bacon)
        self.assertEqual(t2.content_object, bacon)

    def test_add_rejects_unsaved_objects(self):
        t1 = TaggedItem(content_object=self.quartz, tag="shiny")
        msg = "<TaggedItem: shiny> instance isn't saved. Use bulk=False or save the object first."
        with self.assertRaisesMessage(ValueError, msg):
            self.bacon.tags.add(t1)

    def test_set(self):
        bacon = Vegetable.objects.create(name="Bacon", is_yucky=False)
        fatty = bacon.tags.create(tag="fatty")
@@ -266,13 +292,13 @@ class GenericRelationsTests(TestCase):
        bacon.tags.set([])
        self.assertQuerysetEqual(bacon.tags.all(), [])

        bacon.tags.set([fatty, salty], clear=True)
        bacon.tags.set([fatty, salty], bulk=False, clear=True)
        self.assertQuerysetEqual(bacon.tags.all(), [
            "<TaggedItem: fatty>",
            "<TaggedItem: salty>",
        ])

        bacon.tags.set([fatty], clear=True)
        bacon.tags.set([fatty], bulk=False, clear=True)
        self.assertQuerysetEqual(bacon.tags.all(), [
            "<TaggedItem: fatty>",
        ])
Loading