Fixed #26706 -- Made RelatedManager modification methods clear prefetch_related() cache.

This commit is contained in:
Yoong Kang Lim 2016-06-05 22:15:00 +10:00 committed by Tim Graham
parent ea65c7cb48
commit d30febb4e5
7 changed files with 124 additions and 2 deletions

View File

@ -577,6 +577,12 @@ def create_reverse_many_to_one_manager(superclass, rel):
queryset._known_related_objects = {self.field: {self.instance.pk: self.instance}}
return queryset
def _remove_prefetched_objects(self):
try:
self.instance._prefetched_objects_cache.pop(self.field.related_query_name())
except (AttributeError, KeyError):
pass # nothing to clear from cache
def get_queryset(self):
try:
return self.instance._prefetched_objects_cache[self.field.related_query_name()]
@ -606,6 +612,7 @@ def create_reverse_many_to_one_manager(superclass, rel):
return queryset, rel_obj_attr, instance_attr, False, cache_name
def add(self, *objs, **kwargs):
self._remove_prefetched_objects()
bulk = kwargs.pop('bulk', True)
objs = list(objs)
db = router.db_for_write(self.model, instance=self.instance)
@ -680,6 +687,7 @@ def create_reverse_many_to_one_manager(superclass, rel):
clear.alters_data = True
def _clear(self, queryset, bulk):
self._remove_prefetched_objects()
db = router.db_for_write(self.model, instance=self.instance)
queryset = queryset.using(db)
if bulk:
@ -856,6 +864,12 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
queryset = queryset.using(self._db)
return queryset._next_is_sticky().filter(**self.core_filters)
def _remove_prefetched_objects(self):
try:
self.instance._prefetched_objects_cache.pop(self.prefetch_cache_name)
except (AttributeError, KeyError):
pass # nothing to clear from cache
def get_queryset(self):
try:
return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
@ -909,7 +923,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
"intermediary model. Use %s.%s's Manager instead." %
(opts.app_label, opts.object_name)
)
self._remove_prefetched_objects()
db = router.db_for_write(self.through, instance=self.instance)
with transaction.atomic(using=db, savepoint=False):
self._add_items(self.source_field_name, self.target_field_name, *objs)
@ -927,6 +941,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
"an intermediary model. Use %s.%s's Manager instead." %
(opts.app_label, opts.object_name)
)
self._remove_prefetched_objects()
self._remove_items(self.source_field_name, self.target_field_name, *objs)
remove.alters_data = True
@ -938,6 +953,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=None, using=db,
)
self._remove_prefetched_objects()
filters = self._build_remove_filters(super(ManyRelatedManager, self).get_queryset().using(db))
self.through._default_manager.using(db).filter(filters).delete()

View File

@ -979,6 +979,18 @@ database.
performance, since you have done a database query that you haven't used. So
use this feature with caution!
Also, if you call the database-altering methods
:meth:`~django.db.models.fields.related.RelatedManager.add`,
:meth:`~django.db.models.fields.related.RelatedManager.remove`,
:meth:`~django.db.models.fields.related.RelatedManager.clear` or
:meth:`~django.db.models.fields.related.RelatedManager.set`, on
:class:`related managers<django.db.models.fields.related.RelatedManager>`,
any prefetched cache for the relation will be cleared.
.. versionchanged:: 1.11
The clearing of the prefetched cache described above was added.
You can also use the normal join syntax to do related fields of related
fields. Suppose we have an additional model to the example above::

View File

@ -170,6 +170,14 @@ Related objects reference
``add()``, ``create()``, ``remove()``, and ``set()`` methods are
disabled.
If you use :meth:`~django.db.models.query.QuerySet.prefetch_related`,
the ``add()``, ``remove()``, ``clear()``, and ``set()`` methods clear
the prefetched cache.
.. versionchanged:: 1.11
The clearing of the prefetched cache described above was added.
Direct Assignment
=================

View File

@ -357,6 +357,13 @@ Miscellaneous
* The ``checked`` attribute rendered by form widgets now uses HTML5 boolean
syntax rather than XHTML's ``checked='checked'``.
* :meth:`RelatedManager.add()
<django.db.models.fields.related.RelatedManager.add>`,
:meth:`~django.db.models.fields.related.RelatedManager.remove`,
:meth:`~django.db.models.fields.related.RelatedManager.clear`, and
:meth:`~django.db.models.fields.related.RelatedManager.set` now
clear the ``prefetch_related()`` cache.
.. _deprecated-features-1.11:
Features deprecated in 1.11

View File

@ -518,6 +518,40 @@ class ManyToManyTests(TestCase):
self.assertQuerysetEqual(self.a4.publications.all(), [])
self.assertQuerysetEqual(self.p2.article_set.all(), ['<Article: NASA finds intelligent life on Earth>'])
def test_clear_after_prefetch(self):
a4 = Article.objects.prefetch_related('publications').get(id=self.a4.id)
self.assertQuerysetEqual(a4.publications.all(), ['<Publication: Science News>'])
a4.publications.clear()
self.assertQuerysetEqual(a4.publications.all(), [])
def test_remove_after_prefetch(self):
a4 = Article.objects.prefetch_related('publications').get(id=self.a4.id)
self.assertQuerysetEqual(a4.publications.all(), ['<Publication: Science News>'])
a4.publications.remove(self.p2)
self.assertQuerysetEqual(a4.publications.all(), [])
def test_add_after_prefetch(self):
a4 = Article.objects.prefetch_related('publications').get(id=self.a4.id)
self.assertEqual(a4.publications.count(), 1)
a4.publications.add(self.p1)
self.assertEqual(a4.publications.count(), 2)
def test_set_after_prefetch(self):
a4 = Article.objects.prefetch_related('publications').get(id=self.a4.id)
self.assertEqual(a4.publications.count(), 1)
a4.publications.set([self.p2, self.p1])
self.assertEqual(a4.publications.count(), 2)
a4.publications.set([self.p1])
self.assertEqual(a4.publications.count(), 1)
def test_add_then_remove_after_prefetch(self):
a4 = Article.objects.prefetch_related('publications').get(id=self.a4.id)
self.assertEqual(a4.publications.count(), 1)
a4.publications.add(self.p1)
self.assertEqual(a4.publications.count(), 2)
a4.publications.remove(self.p1)
self.assertQuerysetEqual(a4.publications.all(), ['<Publication: Science News>'])
def test_inherited_models_selects(self):
"""
#24156 - Objects from child models where the parent's m2m field uses

View File

@ -43,7 +43,7 @@ class City(models.Model):
@python_2_unicode_compatible
class District(models.Model):
city = models.ForeignKey(City, models.CASCADE)
city = models.ForeignKey(City, models.CASCADE, related_name='districts', null=True)
name = models.CharField(max_length=50)
def __str__(self):

View File

@ -624,3 +624,48 @@ class ManyToOneTests(TestCase):
# doesn't exist should be an instance of a subclass of `AttributeError`
# refs #21563
self.assertFalse(hasattr(Article(), 'reporter'))
def test_clear_after_prefetch(self):
c = City.objects.create(name='Musical City')
District.objects.create(name='Ladida', city=c)
city = City.objects.prefetch_related('districts').get(id=c.id)
self.assertQuerysetEqual(city.districts.all(), ['<District: Ladida>'])
city.districts.clear()
self.assertQuerysetEqual(city.districts.all(), [])
def test_remove_after_prefetch(self):
c = City.objects.create(name='Musical City')
d = District.objects.create(name='Ladida', city=c)
city = City.objects.prefetch_related('districts').get(id=c.id)
self.assertQuerysetEqual(city.districts.all(), ['<District: Ladida>'])
city.districts.remove(d)
self.assertQuerysetEqual(city.districts.all(), [])
def test_add_after_prefetch(self):
c = City.objects.create(name='Musical City')
District.objects.create(name='Ladida', city=c)
d2 = District.objects.create(name='Ladidu')
city = City.objects.prefetch_related('districts').get(id=c.id)
self.assertEqual(city.districts.count(), 1)
city.districts.add(d2)
self.assertEqual(city.districts.count(), 2)
def test_set_after_prefetch(self):
c = City.objects.create(name='Musical City')
District.objects.create(name='Ladida', city=c)
d2 = District.objects.create(name='Ladidu')
city = City.objects.prefetch_related('districts').get(id=c.id)
self.assertEqual(city.districts.count(), 1)
city.districts.set([d2])
self.assertQuerysetEqual(city.districts.all(), ['<District: Ladidu>'])
def test_add_then_remove_after_prefetch(self):
c = City.objects.create(name='Musical City')
District.objects.create(name='Ladida', city=c)
d2 = District.objects.create(name='Ladidu')
city = City.objects.prefetch_related('districts').get(id=c.id)
self.assertEqual(city.districts.count(), 1)
city.districts.add(d2)
self.assertEqual(city.districts.count(), 2)
city.districts.remove(d2)
self.assertEqual(city.districts.count(), 1)