如何进行多对多Django查询以查找具有两个特定作者的书籍?

23 投票
5 回答
9406 浏览
提问于 2025-04-16 13:40

我有一个问题,需要筛选出恰好两个特定ID的作者。

理论上,

Book.objects.filter(author__id=1, author__id=2). 

这是不可能的。

我该如何解决这个问题呢?

谢谢,
米奇

5 个回答

0

当你使用Postgres作为数据库时,可以用 ArrayAgg 这个功能把作者的ID收集到一个数组里,然后根据这个数组进行筛选:

from django.contrib.postgres.aggregates import ArrayAgg

qs = (
    Book.objects
    .annotate(
        authors=ArrayAgg("author__id", distinct=True, ordering=("author__id",))
    )
    .filter(authors=[1, 2])
)

我使用了 distinctordering 来确保ID不会重复,并且总是保持相同的顺序。

这个方法在生成的SQL查询中只会产生一个 JOIN,而如果你使用多个 .filter() 方法的话,就会在查询中产生多个 JOIN

1

有新的问题指向这个问题,认为它是重复的,所以这里有一个更新的答案(针对一个特定的后端)。

如果后端是Postgres,你需要的SQL语句是(假设多对多表叫做 bookauthor):

SELECT *
FROM book
WHERE
    (SELECT ARRAY_AGG(bookauthor.author_id)
     FROM bookauthor
     WHERE bookauthor.book_id = book.id) = Array[1, 2];

你可以让Django生成几乎相同的SQL语句。

首先,运行 pip install django-sql-utils 来安装这个工具。然后创建这个 Array 类:

from django.db.models import Func

class Array(Func):
    function = 'ARRAY'
    template = '%(function)s[%(expressions)s]'

现在你可以写你的ORM查询集了:

from sql_util.utils import SubqueryAggregate
from django.contrib.postgres.aggregates import ArrayAgg

books = Book.objects.annotate(
            author_ids=SubqueryAggregate('author__id', Aggregate=ArrayAgg)
        ).filter(author_ids=Array(1, 2))
51

一开始可能不太好理解,但答案就在我们面前。

Book.objects.filter(author__id=1).filter(author__id=2)

如果你想要完全匹配的结果,可以进一步筛选,只找那些恰好有两个作者的项目。

Book.objects.annotate(count=Count('author')).filter(author__id=1)\
                .filter(author__id=13).filter(count=2)

如果你想要动态的完全匹配,试试这样做怎么样?

def get_exact_match(model_class, m2m_field, ids):
    query = model_class.objects.annotate(count=Count(m2m_field))\
                .filter(count=len(ids))
    for _id in ids:
        query = query.filter(**{m2m_field: _id})
    return query

matches = get_exact_match(MyModel, 'my_m2m_field', [1, 2, 3, 4])

# matches is still an unevaluated queryset, so you could run more filters
# without hitting the database.

撰写回答