Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trouble with chaining Q() objects with the same argument in Django's ORM

I am working on creating a cocktail recipe app as a learning exercise.

I am trying to create a filter through Django's Rest Framework that accepts a string of ingredient IDs through a query parameter (?=ingredients_exclusive=1,3,4), and then searches for all recipes that have all of those ingredients. I would like to search for “All cocktails that have both rum and grenadine” and then also, separately “All cocktails that have rum, and all cocktails that have grendaine.”

The three models in my app are Recipes, RecipeIngredients, and IngredientTypes. Recipes (Old Fashioned) have multiple RecipeIngredients (2oz of Whiskey), and RecipeIngredients are all of a Ingredient Type (Whiskey). I will eventually change the RecipeIngredient to a through model depending on how far I decide to take this.

The list can be of a variable length, so I cannot just chain together filter functions. I have to loop through the list of ids and then build a Q().

However, I'm having some issues. Through the Django Shell, I have done this:

>>> x = Recipe.objects.all()
>>> q = Q(ingredients__ingredient_type=3) & Q(ingredients__ingredient_type=7)
>>> x.filter(q)
<QuerySet []>
>>> x.filter(ingredients__ingredient_type=3).filter(ingredients__ingredient_type=7)
<QuerySet [<Recipe: Rum and Tonic>]>

So here's my question: Why is the Q object that ANDs the two queries different than the chained filters of same object?

I've read through the "Complex lookups with Q objects" in the Django documentation and it doesn't seem to help.

Just for reference, here are my filters in Filters.py.

The "OR" version of this command is working properly:

class RecipeFilterSet(FilterSet):
    ingredients_inclusive = django_filters.CharFilter(method='filter_by_ingredients_inclusive')
    ingredients_exclusive = django_filters.CharFilter(method='filter_by_ingredients_exclusive')

    def filter_by_ingredients_inclusive(self, queryset, name, value):
        ingredients = value.split(',')
        q_object = Q()
        for ingredient in ingredients:
            q_object |= Q(ingredients__ingredient_type=ingredient)
        return queryset.filter(q_object).distinct()

    def filter_by_ingredients_exclusive(self, queryset, name, value):
        ingredients = value.split(',')
        q_object = Q()
        for ingredient in ingredients:
            q_object &= Q(ingredients__ingredient_type=ingredient)
        return queryset.filter(q_object).distinct()

    class Meta:
        model = Recipe
        fields = ()

I've also included my models below:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
​
from django.db import models
​
​
class IngredientType(models.Model):
  name = models.CharField(max_length=256)
​
  CATEGORY_CHOICES = (
    ('LIQUOR', 'Liquor'),
    ('SYRUP', 'Syrup'),
    ('MIXER', 'Mixer'),
  )
​
  category = models.CharField(
    max_length=128, choices=CATEGORY_CHOICES, default='MIXER')
​
  def __str__(self):
    return self.name
​
​
class Recipe(models.Model):
  name = models.CharField(max_length=256)
​
  def __str__(self):
    return self.name
​
​
class RecipeIngredient(models.Model):
  ingredient_type = models.ForeignKey(IngredientType, on_delete=models.CASCADE, related_name="ingredients")
  quantity = models.IntegerField(default=0)
  quantity_type = models.CharField(max_length=256)
  recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, related_name="ingredients")
​
  @property
  def ingredient_type_name(self):
    return self.ingredient_type.name
​
  @property
  def ingredient_type_category(self):
    return self.ingredient_type.category
​
  def __str__(self):
    return f'{self.quantity}{self.quantity_type} of {self.ingredient_type}'

Any help would be very much appreciated!

like image 373
Neil Shah Avatar asked Mar 27 '26 16:03

Neil Shah


1 Answers

The difference between the two approaches to filter() is described in Spanning multi-valued relationships:

Everything inside a single filter() call is applied simultaneously to filter out items matching all those requirements.... For multi-valued relations, they apply to any object linked to the primary model, not necessarily those objects that were selected by an earlier filter() call.

The example in the documentation makes it more clear. I'll rewrite it in terms of your problem:

To select all recipes that contain an ingredient with both type 3 and type 7, we would write:

Recipe.objects.filter(ingredients__ingredient_type=3, ingredients__ingredient_type=7)

That is of course impossible in your model, so this would return an empty queryset, just like your Q example with AND.

To select all recipes that contain an ingredient with type 3 as well as an ingredient with type 7, we would write:

Recipe.objects.filter(ingredients__ingredient_type=3).filter(ingredients__ingredient_type=7)

It's not especially intuitive, but they needed a way to distinguish these two cases and this is what they came up with.


Back to your problem, the OR case can be made simpler by using the in operator:

Recipe.objects.filter(ingredients__ingredient_type__in=[3, 7]).distinct()

The AND case is complicated because it's a condition that involves multiple rows. A simple approach would be to just take the OR version above and further process it in Python to find the subset that has all the ingredients.

A query approach that should work involves annotation with Count. This is untested, but something like:

Recipe.objects.annotate(num_ingredients=Count("ingredients", 
                            filter=Q(ingredients__ingredient_type__in=[3, 7]))
              .filter(num_ingredients=2)
like image 114
Kevin Christopher Henry Avatar answered Mar 29 '26 06:03

Kevin Christopher Henry



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!