Django фильтрует отношение m2m по списку входных данных (которые все должны совпадать)

Давайте возьмем несколько моделей Store и Book в качестве примеров:

class Book(Model):
    title = CharField(...)
    ...

class Store(Model):
    books = ManyToManyField('Book', blank=True, related_name='stores')
    ....

Я получаю список названий книг и должен вернуть магазины, связанные с этими книгами. Мне нужна опция как для запроса AND, так и для запроса OR.

Запрос OR довольно прост; нам нужно, чтобы хранилище совпадало только один раз:

Store.objects.filter(book__title__in=book_titles)

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

from django.db.models import Q

filtering = Q()
for book_title in book_title_list:
    filtering &= Q(id__in=Book.objects.get(title=book_title).stores)
Store.objects.filter(filtering)

Это фактически создает OUTER JOIN и SELECT в предложении WHERE для каждого названия книги, что при значении 2 или 3 не так уж много, но определенно нежелательно, если пользовательский ввод не ограничен.

Без явного зацикливания и добавления Q объектов, подобных этому, мне еще предстоит получить запрос, который действительно работает. Чаще всего запрос либо оценивает только одну строку отношения m2m, либо ведет себя аналогично запросу OR. Напомним, что для выполнения запроса AND необходимо, чтобы все возвращенные хранилища были связаны с всеми книгами, названия которых были указаны.

Я пробовал вмешиваться в классы и методы Subquery, CASE, WHERE и annotate, предоставляемые Django, но я ни в коем случае не эксперт и столкнулся только с неудачей. В какой-то момент я подумал, что использование annotate с увеличивающимся значением для каждого совпадения (учитывая, что длина списка известна) могло бы сработать, но мне еще предстоит найти какой-то способ заставить это работать.

Для дополнительной сложности я также не могу выполнить цикл или получить доступ к любому из наборов запросов, поскольку количество элементов неизвестно, но определенно составляет более 100 тысяч.

Вы можете проверить, совпадают ли все book с:

from django.db.models import Count

book_titles_set = set(book_titles)
Store.objects.filter(book__title__in=book_titles_set).annotate(
    nbook=Count('book')
).filter(nbook=len(book_titles_set))
Вернуться на верх