#35677: Unexpected behaviour of Prefetch with queryset filtering on a through 
model
-------------------------------------+-------------------------------------
     Reporter:  David Glenck         |                     Type:
                                     |  Uncategorized
       Status:  new                  |                Component:  Database
                                     |  layer (models, ORM)
      Version:  5.1                  |                 Severity:  Normal
     Keywords:  Prefetch,            |             Triage Stage:
  prefetch_related, many-to-many     |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------
 Currently prefetching many-to-many objects with a queryset that filters
 based on fields in the through model, will return different results than
 applying the same filter without prefetching.

 Assume the following many-to-many relationship with a through model:


 {{{
 class Subscriber(models.Model):
     name = models.CharField(max_length=255)
     subscriptions = models.ManyToManyField('Subscription',
 related_name='subscribers', through='Status')


 class Subscription(models.Model):
     provider_name = models.CharField(max_length=255)


 class Status(models.Model):
     subscriber = models.ForeignKey(Subscriber, related_name='status',
 on_delete=models.CASCADE)
     subscription = models.ForeignKey(Subscription, related_name='status',
 on_delete=models.CASCADE)
     is_active = models.BooleanField(default=True)
 }}}

 Populated with the following data:


 {{{
     subscriber1 = Subscriber.objects.create(name="Sofia")
     subscriber2 = Subscriber.objects.create(name="Peter")
     subscription1 = Subscription.objects.create(provider_name="ABC")
     subscription2 = Subscription.objects.create(provider_name="123")
     Status.objects.create(subscriber=subscriber1,
 subscription=subscription1, is_active=True)
     Status.objects.create(subscriber=subscriber1,
 subscription=subscription1, is_active=True)
     Status.objects.create(subscriber=subscriber1,
 subscription=subscription2, is_active=False)
     Status.objects.create(subscriber=subscriber2,
 subscription=subscription1, is_active=True)
     Status.objects.create(subscriber=subscriber2,
 subscription=subscription2, is_active=True)
 }}}

 To get the active subscriptions of Sofia I can do this:

 {{{
 subscriber1.subscriptions.filter(status__is_active=True)
 }}}

 which gives me as expected twice the first subscription

 Now, I try to prefetch the active subscriptions like this:

 {{{
 prefetched = Subscriber.objects.filter(pk__in=[subscriber1.pk,
 subscriber2.pk]).prefetch_related(
     Prefetch(
         'subscriptions',
         queryset=Subscription.objects.filter(status__is_active=True),
         to_attr='active_subscriptions'
     )
 )
 prefetched[0].active_subscriptions
 }}}

 But if I do this, I get queryset with 4 times instead of 2 times the first
 subscription.

 Looking into the source I found, that in the first case, the internal
 filter of the related_descriptor is made "sticky", such that it will be
 combined with the next filter that is applied, as if only one filter was
 used. So it behaves equivalent to:

 {{{
 Subscription.objects.filter(subscribers=subscriber1.id,
 status__is_active=True)
 }}}

 The prefetch on the other hand doesn't do any magic to stick the filters
 together and it will behave like this:
 {{{
 
Subscription.objects.filter(subscribers=subscriber1.id).filter(status__is_active=True)
 }}}
 which, as well documented, is behaving differently for many-to-many
 relationships: https://docs.djangoproject.com/en/5.0/topics/db/queries
 /#spanning-multi-valued-relationships

 Ideally the first filter on the queryset would also stick to the internal
 filter. Or at least there should be an easy way to change the behaviour,
 when needed.
-- 
Ticket URL: <https://code.djangoproject.com/ticket/35677>
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 django-updates+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/010701914d152c83-1c6cbc5f-3fc6-4962-be5a-822b16e8e72d-000000%40eu-central-1.amazonses.com.

Reply via email to