#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.