Django ManyToMany chained queryset not giving ANDed result

时间:2018-06-04 17:13:41

标签: django m2m

This is leaving me baffled for a while.

I've a ManyToMany Relationship established through a table as shown:

class Coupon(TimeStampedUUIDModel):
    venue = models.ManyToManyField('venues.Venue', through='VenueCouponConfig', related_name='coupons')


    class Meta:
        verbose_name = _('Coupon')
        verbose_name_plural = _('Coupons')
        ordering = ('-created_at', )


class VenueCouponConfig(UUIDModel):
    venue = models.ForeignKey(
        'venues.Venue', null=True, blank=True, on_delete=models.SET_NULL
    )
    coupon = models.ForeignKey(
        'Coupon', null=True, blank=True, on_delete=models.SET_NULL
    )
    is_activated = models.BooleanField(_('Activate Coupon'), null=False, blank=True)

Essentially, I wanted to make Coupons associated with multiple venues, so I created through table to have is_activated flag per venue which describes if certain coupon is activated for a venue or not.

Now, I've this API which lists all the coupons in particular venue like:

...
venue = get_object_or_404(self.queryset, pk=pk)
qs = Coupon.objects.all()
qs = qs.prefetch_related(Prefetch('venuecouponconfig_set'))
qs = qs.filter(venue=venue)
qs = qs.filter(venuecouponconfig__is_activated=True)
qs = qs.order_by('created_at')
...

Now the evaluation of this API gives me some coupons multiple times.

But if I attach the filter like:

...
venue = get_object_or_404(self.queryset, pk=pk)
qs = Coupon.objects.all()
qs = qs.prefetch_related(Prefetch('venuecouponconfig_set'))
qs = qs.filter(venue=venue, venuecouponconfig__is_activated=True)
qs = qs.order_by('created_at')
...

This gives me correct result and coupons appear single time.

Does chain filters work differently with ManyToManyField? Did it not go for doing an AND unless both the things were mentioned in a single filter query?

Am I missing something here?

Let me know if any more info is needed.

1 个答案:

答案 0 :(得分:1)

By making separate filter() calls you create independent filters.

qs = Coupon.objects.all()
qs = qs.filter(venue=venue)
qs = qs.filter(venuecouponconfig__is_activated=True)

→ filter all coupons that match the venue.
→ AND filter all coupons that have one of their relations with is_activated true.

qs = Coupon.objects.all()
qs = qs.filter(venue=venue, venuecouponconfig__is_activated=True)

→ filter all coupons that match the venue AND that specific venue has is_activated true.

As you can see, in both cases you're doing a logical AND, but the point at which it applies is different.

On the technical side, the first request creates two separate JOINs and applies each filter on one, while the second request creates only one JOIN and applies both filters to it.

As a rule of thumb, each filter() call creates a distinct set of JOIN that are not re-used by subsequent calls.

(I removed prefetch_related as it is irrelevant to the result set)