#36282: Prefetched relations are not available used from parent classes in all
cases
------------------------------+-----------------------------------------
     Reporter:  Take Weiland  |                     Type:  Uncategorized
       Status:  new           |                Component:  Uncategorized
      Version:  5.1           |                 Severity:  Normal
     Keywords:                |             Triage Stage:  Unreviewed
    Has patch:  0             |      Needs documentation:  0
  Needs tests:  0             |  Patch needs improvement:  0
Easy pickings:  0             |                    UI/UX:  0
------------------------------+-----------------------------------------
 When using `prefetch_related` to prefetch relations, Django makes some
 attempt to use prefetched data present in parent models, but it is very
 limited. It is only implemented for `ForeignKey` and only works for the
 immediate parent.

 Assuming this model hierarchy:

 {{{
 class Related(models.Model):
     pass


 class GrandParent(models.Model):
     name = models.CharField(max_length=50)
     gp_fk = models.ForeignKey(Related, null=True,
 on_delete=models.CASCADE, related_name='gp_fk_rel')
     gp_m2m = models.ManyToManyField(Related, related_name='gp_m2m_rel')


 class Parent(GrandParent):
     pass


 class Child(Parent):
     pass
 }}}

 I have written these test cases:

 {{{
 class PrefetchRelatedWorksWithInheritance(TestCase):

     @classmethod
     def setUpTestData(cls):
         cls.related1 = Related.objects.create()
         cls.related2 = Related.objects.create()
         cls.related3 = Related.objects.create()

         cls.child = Child.objects.create(
             gp_fk=cls.related1,
         )
         cls.m2m_child = Child.objects.create()
         cls.m2m_child.gp_m2m.set([cls.related1, cls.related2,
 cls.related3])

     def test_parent_fk_available_in_child(self):
         qs =
 GrandParent.objects.select_related('parent').prefetch_related(
             'gp_fk'
         ).filter(pk=self.child.pk)
         with self.assertNumQueries(2):
             results = list(qs)
             self.assertEqual(len(results), 1)
             # Works, Parent can look into its GrandParent and find the
 prefetched data
             self.assertEqual(results[0].parent.gp_fk, self.related1)

     def test_grandparent_fk_available_in_child(self):
         qs = GrandParent.objects.select_related('parent',
 'parent__child').prefetch_related(
             'gp_fk'
         ).filter(pk=self.child.pk)
         with self.assertNumQueries(2):
             results = list(qs)
             self.assertEqual(len(results), 1)
             # Causes extra query, Child only looks in Parent, not in
 GrandParent
             self.assertEqual(results[0].parent.child.gp_fk, self.related1)

     def test_parent_m2m_available_in_child(self):
         qs =
 GrandParent.objects.select_related('parent').prefetch_related(
             'gp_m2m'
         ).filter(pk=self.m2m_child.pk)
         with self.assertNumQueries(2):
             results = list(qs)
             self.assertEqual(len(results), 1)
             # Causes extra query, M2M never looks in its parents
             self.assertEqual(set(results[0].parent.gp_m2m.all()),
 {self.related1, self.related2, self.related3})

     def test_grandparent_m2m_available_in_child(self):
         qs = GrandParent.objects.select_related('parent',
 'parent__child').prefetch_related(
             'gp_m2m'
         ).filter(pk=self.m2m_child.pk)
         with self.assertNumQueries(2):
             results = list(qs)
             self.assertEqual(len(results), 1)
             # Causes extra query, M2M never looks in its parents
             self.assertEqual(set(results[0].parent.child.gp_m2m.all()),
 {self.related1, self.related2, self.related3})

 }}}

 Only the first of the tests passes.
 For ForeignKeys the reason is here:
 
https://github.com/django/django/blob/c3a23aa02faa1cf1d32e43d66858e793cd9ecac4/django/db/models/fields/related_descriptors.py#L229-L240
 This only looks into the immediate parent, not any grandparents.

 For ManyToManyField the culprit is here:
 
https://github.com/django/django/blob/c3a23aa02faa1cf1d32e43d66858e793cd9ecac4/django/db/models/fields/related_descriptors.py#L1094-L1098
 It only looks in the instance's _prefetched_objects_cache, when it could
 walk up to its parents, just like ForwardManyToOneDescriptor does.

 I stumbled upon this when implementing support for
 [https://github.com/jazzband/django-model-utils/pull/639 prefetch_related
 in Django-Model-Utils' InheritanceManager]. It automatically returns
 subclasses form a custom QuerySet iterable. But those subclass-instances
 then do not see the prefetched data of their parent, so in my fix I had to
 manually copy the caches, so that Django would find them.

 I would be willing to work on a patch to make this work out of the box in
 Django.
-- 
Ticket URL: <https://code.djangoproject.com/ticket/36282>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/01070195e6550f8a-af88b173-d51e-4940-a915-2b5d02e94393-000000%40eu-central-1.amazonses.com.

Reply via email to