Django反向M2M查询

时间:2018-09-20 14:48:09

标签: python django

使用https://docs.djangoproject.com/en/dev/topics/db/queries/#making-queries中的模型,并进行少量修改:

from django.db import models

class Blog(models.Model):
    name = models.CharField(max_length=100)

class Author(models.Model):
    name = models.CharField(max_length=200)
    joined = models.DateField()

    def __str__(self):
        return self.name

class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    authors = models.ManyToManyField(Author)
    rating = models.IntegerField()

我想创建一个从Author到Entries的词典,作者今年加入了该词典,并且Entry的评分为4或更高。结果字典的结构应如下所示:

author_entries = {author1: [set of entries], author2: [set of entries], etc.}

在访问数据库的时间少于3分钟(或至少与作者或条目的数量不成比例)时。

我的第一次尝试(db hits ==作者数量,100作者100 db-hits):

    res = {}
    authors = Author.objects.filter(joined__year=date.today().year)

    for author in authors:
        res[author] = set(author.entry_set.filter(rating__gte=4))

第二次尝试,尝试一次读取条目:

    res = {}
    authors = Author.objects.filter(joined__year=date.today().year)
    entries = Entry.objects.select_related().filter(rating__gte=4, authors__in=authors)

    for author in authors:
        res[author] = {e for e in entries if e.authors.filter(pk=author.pk)}

这甚至更糟,有100位作者,198次数据库命中(最初的第二次尝试使用了{e for e in entries if author in e.authors},但是Django却没有。

我发现的唯一方法涉及原始SQL(4个db-hits):

    res = {}
    _authors = Author.objects.filter(joined__year=date.today().year)
    _entries = Entry.objects.select_related().filter(rating__gte=4, authors__in=_authors)
    authors = {a.id: a for a in _authors}
    entries = {e.id: e for e in _entries}
    c = connection.cursor()
    c.execute("""
        select entry_id, author_id 
        from sampleapp_entry_authors
        where author_id in (%s)
    """ % ','.join(str(v) for v in authors.keys()))

    res = {a: set() for a in _authors}
    for eid, aid in c.fetchall():
        if eid in entries:
            res[authors[aid]].add(entries[eid])

(在c.execute(..)调用中使用字符串替换的道歉–我找不到where in ?调用所需的sqlite语法)。

还有更多类似Django的方式吗?

我用我正在使用的代码(https://github.com/thebjorn/revm2m)创建了一个git repo,测试在https://github.com/thebjorn/revm2m/blob/master/revm2m/sampleapp/tests.py

1 个答案:

答案 0 :(得分:3)

您可以使用Prefetch-object [Django-doc]

from django.db.models import Prefetch

good_ratings = Prefetch(
    'entry_set',
    queryset=Entry.objects.filter(rating__gte=4),
    to_attr='good_ratings'
)

authors = Author.objects.filter(
    joined__year=date.today().year
).prefetch_related(
    good_ratings
)

现在Author中的authors对象将具有一个预先加载的额外属性good_ratingsto_attr对象的Prefetch的值) QuerySet包含评级大于或等于4的Entry

因此您可以像这样对它们进行后处理:

res = {
    author: set(author.good_ratings)
    for author in authors
}

尽管由于Author个对象(不是从这个QuerySet开始的对象)已经带有属性,所以反正可能没有太多用途。