Учитывая эти модели 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/
Но почему я думаю, что это должно быть уже написано? Как это может быть таким уникальным и новым вариантом использования?
@ Бернд Вехнер Но, может быть, я чего-то не понимаю. Если: category__contains=[2,3] категории вернут идентификатор, который вы хотите сравнить со строкой содержимого. Как это понять или у вас категории возвращают строки?
@Блэкдор. Действительно. Но categories
— родственное название категории в вещах. По умолчанию будет category_set
.
@вопроситель. Извинения. Как было отмечено в случае с Blackdoor, categories
— это родственное имя категории ManyToManyField, поэтому Fields.categories — это набор категорий (на самом деле это ManyToManyDescriptor в модели и ManyRelatedManager в объекте). Я уточню вопрос.
Приносим извинения за путаницу и надеемся, что разъясненный вопрос будет полезен.
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 сейчас есть несколько попыток, включая предложенные вами идеи. Увы, не повезло.
Thing.objects.filter(Q(categories=2) & Q(categories=3)) возвращает пустой QuerySet, поскольку одновременно должны быть категории=2 и категории=3. То есть id 2 не может быть id 3. Другими словами. Почему вы не хотите использовать: category__in=[2, 3]? Вероятно, вы хотите получить thing.categories.all() без использования цикла для каждой вещи? Если да, то я не знаю как это сделать, пробовала здесь, но она с петлей.
Да, довольно очевидно, почему Q(categories=2) & Q(categories=3)
возвращает пустое значение, я предлагаю это только как пример того, что пробовали и не работают (я имею в виду, что Q(categories=2) | Q(categories=3)
в конце концов работает так, как ожидалось, так что наивный намек на переход от ИЛИ к И — это просто так. Никаких ожиданий, это будет работать. categories__in=[2, 3]
хорошо, за исключением того, что это точно так же, как Q(categories=2) | Q(categories=3)
, и это попытка 2 в образце в ОП, он не возвращает желаемого. И да, поиск запроса (проверка каждой вещи не имеет смысла).
Я правильно вас понял, вам нужно получить, как в вашем примере, что вернуло "База данных содержит"?
База данных создается кодом в OP, это просто список всех вещей в базе данных и их категорий, поэтому я могу оценить успех набора запросов. Я вижу на глаз, что только вещь 4 и вещь 5 находятся в категории 2 и в категории 3. И это результат, который мне нужен. Я должен надеяться, что код и песочница постепенно очищаются, или?
@ Бернд Вехнер обновил ответ.
Великолепно! Если вы проверите песочницу, я реализовал ее там в качестве теста. Две мелочи: 1) __get не нужен, работает просто cat_count=2. Я думаю, это потому, что фильтр __in
в аннотации возвращает только кошек в наборе (дополнительные функции неявно игнорируются, поэтому gte
работает неявно) 2) Нет необходимости в .values()
, я счастлив просто возвращать вещи ;-). Но в остальном идеальное понимание — это аргумент фильтра для агрегатора Count! Я проверю сгенерированный SQL и подтвержу, как будет, и сообщу, если обнаружу проблему.
Проверил SQL, все отлично. Огромное спасибо! Этот фильтр на подсчете был уловкой. Хотя, как уже отмечалось _gte
не нужно. просто = нормально.
Неправильное использование с Q&Q и contains, потому что один объект имеет уникальный идентификатор, поэтому у него никогда не бывает двух разных идентификаторов.