Запрос Django Many2Many для поиска всех «вещей» в группе «категорий»

Учитывая эти модели Django:

from django.db import models

class Thing(models.model):
    name = models.CharField('Name of the Thing')

class Category(models.model):
    name = models.CharField('Name of the Category')
    things = models.ManyToManyField(Thing, verbose_name='Things', related_name='categories')

Обратите внимание, что все категории, в которых находится Вещь, можно найти:

thing = Thing.objects.get(id=1) # for example
cats = thing.categories.all() # A QuerySet

Я действительно изо всех сил пытаюсь создать набор запросов, который возвращает все вещи во всех заданных категориях.

Допустим, у нас есть 5 категорий с идентификаторами 1, 2, 3, 4, 5.

И скажем, у меня есть подмножество категорий:

my_cats = Category.objects.filter(id__in=[2,3])

Я хочу найти все вещи, которые находятся, скажем, в категориях 2 и 3.

Я могу найти все вещи в категории 2 ИЛИ 3 достаточно легко. Например это:

Thing.objects.filter(categories__in=[2,3])

кажется, возвращает именно это, Вещи в категории 2 ИЛИ 3.

И что-то вроде:

Thing.objects.filter(Q(categories=2)|Q(categories=3))

также, но это ничего не возвращает:

Thing.objects.filter(Q(categories=2)&Q(categories=3))

Я мог бы представить что-то вроде:

Thing.objects.filter(categories__contains=[2,3])

но, конечно, это мечта, поскольку contains работает со строками, а не с наборами ManyToMany.

Есть ли здесь стандартный трюк, который мне не хватает?

Я развернул здесь песочницу, чтобы протестировать и продемонстрировать:

https://codesandbox.io/p/sandbox/django-m2m-test-cizmud

Он реализует эту простую пару моделей и заполняет базу данных небольшим набором вещей и категорий, а также тестирует запросы. Вот ее последнее состояние:

print("Database contains:")
for thing in Thing.objects.all():
    print(
        f"\t{thing.name} in categorties {[c.id for c in thing.categories.all()]}")
print()

# This works fine. Prints:
# Cat1 OR Cat2: ['Thing 1', 'Thing 5', 'Thing 4']
things = Thing.objects.filter(
    Q(categories=1) | Q(categories=2)).distinct()
print(f"Cat1 OR Cat2: {[t.name for t in things]}")

# We would love this to return Thing4 and thing5
# The two things in the test data set that are in
# Category 2 and in Category 3.
# But this does not work. It prints:
# Cat2 AND Cat3: []
# because
# What does yield ['Thing 4', 'Thing 5']?
print("\nAiming to to get: ['Thing 4', 'Thing 5']")
things = Thing.objects.filter(
    Q(categories=2) & Q(categories=3)).distinct()
print(f"Try 1: Cat2 AND Cat3: {[t.name for t in things]}")

# This also fails, producing an OR not AND
things = Thing.objects.filter(categories__in=[2, 3]).distinct()
print(f"Try 2: Cat2 AND Cat3: {[t.name for t in things]}")

# Also fails
things = Thing.objects.filter(categories__in=[2, 3])\
                      .filter(categories=2).distinct()
print(f"Try 3: Cat2 AND Cat3: {[t.name for t in things]}")

# Also fails
things = Thing.objects.filter(categories__in=[2, 3], categories=2)\
                      .distinct()
print(f"Try 4: Cat2 AND Cat3: {[t.name for t in things]}")

и его вывод:

Database contains:
        Thing 1 in categorties [1, 2]
        Thing 2 in categorties [3, 4]
        Thing 3 in categorties [5]
        Thing 4 in categorties [2, 3]
        Thing 5 in categorties [1, 2, 3]

Cat1 OR Cat2: ['Thing 1', 'Thing 5', 'Thing 4']

Aiming to to get: ['Thing 4', 'Thing 5']
Try 1: Cat2 AND Cat3: []
Try 2: Cat2 AND Cat3: ['Thing 1', 'Thing 4', 'Thing 5', 'Thing 2']
Try 3: Cat2 AND Cat3: ['Thing 1', 'Thing 4', 'Thing 5']
Try 4: Cat2 AND Cat3: ['Thing 1', 'Thing 4', 'Thing 5']

Я думаю, если я смогу решить это на SQL, мы можем написать нам собственный поиск:

https://docs.djangoproject.com/en/4.2/howto/custom-lookups/

Но почему я думаю, что это должно быть уже написано? Как это может быть таким уникальным и новым вариантом использования?

Неправильное использование с Q&Q и contains, потому что один объект имеет уникальный идентификатор, поэтому у него никогда не бывает двух разных идентификаторов.

Blackdoor 07.05.2023 13:47

@ Бернд Вехнер Но, может быть, я чего-то не понимаю. Если: category__contains=[2,3] категории вернут идентификатор, который вы хотите сравнить со строкой содержимого. Как это понять или у вас категории возвращают строки?

inquirer 07.05.2023 16:02

@Блэкдор. Действительно. Но categories — родственное название категории в вещах. По умолчанию будет category_set.

Bernd Wechner 07.05.2023 22:55

@вопроситель. Извинения. Как было отмечено в случае с Blackdoor, categories — это родственное имя категории ManyToManyField, поэтому Fields.categories — это набор категорий (на самом деле это ManyToManyDescriptor в модели и ManyRelatedManager в объекте). Я уточню вопрос.

Bernd Wechner 07.05.2023 23:00

Приносим извинения за путаницу и надеемся, что разъясненный вопрос будет полезен.

Bernd Wechner 07.05.2023 23:09
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
5
94
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий
Thing.objects.annotate(cat_count=Count('id', filter=Q(categories__in=[2, 3]))).\
        filter(cat_count__gte=2).values('id', 'cat_count')

Попробуй это. cat_count__gte=2 потому что два числа 2, 3.

Увы, Thing.objects.filter(Q(categories__in=[2,3]) & Q(categories=2)) тоже не работает, и я не нашел ничего работающего, и мне нужно решение больше, чем мне нужно, чтобы понять, почему данная идея не работает. Я могу изучить SQL каждого запроса, чтобы понять, как он терпит неудачу и почему. Я изо всех сил пытаюсь найти синтаксис Django, который работает. В песочнице в OP сейчас есть несколько попыток, включая предложенные вами идеи. Увы, не повезло.

Bernd Wechner 09.05.2023 06:52

Thing.objects.filter(Q(categories=2) & Q(categories=3)) возвращает пустой QuerySet, поскольку одновременно должны быть категории=2 и категории=3. То есть id 2 не может быть id 3. Другими словами. Почему вы не хотите использовать: category__in=[2, 3]? Вероятно, вы хотите получить thing.categories.all() без использования цикла для каждой вещи? Если да, то я не знаю как это сделать, пробовала здесь, но она с петлей.

inquirer 09.05.2023 12:13

Да, довольно очевидно, почему Q(categories=2) & Q(categories=3) возвращает пустое значение, я предлагаю это только как пример того, что пробовали и не работают (я имею в виду, что Q(categories=2) | Q(categories=3) в конце концов работает так, как ожидалось, так что наивный намек на переход от ИЛИ к И — это просто так. Никаких ожиданий, это будет работать. categories__in=[2, 3] хорошо, за исключением того, что это точно так же, как Q(categories=2) | Q(categories=3), и это попытка 2 в образце в ОП, он не возвращает желаемого. И да, поиск запроса (проверка каждой вещи не имеет смысла).

Bernd Wechner 09.05.2023 12:19

Я правильно вас понял, вам нужно получить, как в вашем примере, что вернуло "База данных содержит"?

inquirer 09.05.2023 12:27

База данных создается кодом в OP, это просто список всех вещей в базе данных и их категорий, поэтому я могу оценить успех набора запросов. Я вижу на глаз, что только вещь 4 и вещь 5 находятся в категории 2 и в категории 3. И это результат, который мне нужен. Я должен надеяться, что код и песочница постепенно очищаются, или?

Bernd Wechner 09.05.2023 12:30

@ Бернд Вехнер обновил ответ.

inquirer 09.05.2023 16:35

Великолепно! Если вы проверите песочницу, я реализовал ее там в качестве теста. Две мелочи: 1) __get не нужен, работает просто cat_count=2. Я думаю, это потому, что фильтр __in в аннотации возвращает только кошек в наборе (дополнительные функции неявно игнорируются, поэтому gte работает неявно) 2) Нет необходимости в .values(), я счастлив просто возвращать вещи ;-). Но в остальном идеальное понимание — это аргумент фильтра для агрегатора Count! Я проверю сгенерированный SQL и подтвержу, как будет, и сообщу, если обнаружу проблему.

Bernd Wechner 09.05.2023 23:43

Проверил SQL, все отлично. Огромное спасибо! Этот фильтр на подсчете был уловкой. Хотя, как уже отмечалось _gte не нужно. просто = нормально.

Bernd Wechner 10.05.2023 00:49

Другие вопросы по теме