Django prefetch_related 具有多种内容类型的 GenericForeignKey

Django prefetch_related GenericForeignKey with multiple content types

我正在使用 django-activity-stream 来显示最近事件的列表。举个例子,这些人可能是评论者或编辑文章的人。 IE。 GenericForeignKey action_object 可以引用 CommentArticle。我想向 action_object 也显示 link:

<a href="{{ action.action_object.get_absolute_url }}">
{{ action.action_object }}
</a>

问题是这会导致对每个项目的查询,特别是 Comment.get_absolute_url 需要评论的 article,但尚未获取,而 Article.__unicode__ 需要 [=21] =],也没有被提取。

django-activity-stream 已经调用了 prefetch_related('action_object') automatically (related discussion)。 尽管 the docs 说:

,但使用 {{ action.action_object.id }} 进行测试时,这似乎在每个 action_object_content_type 中产生一个查询结果

It also supports prefetching of GenericRelation and GenericForeignKey, however, it must be restricted to a homogeneous set of results. For example, prefetching objects referenced by a GenericForeignKey is only supported if the query is restricted to one ContentType.

而且内容类型不止一种。但是在我上面的用例中,我需要额外的 prefetch_related 调用,例如:

query = query.prefetch_related('action_object__article`, `action_object__revision`)

但这会抱怨,因为 Articles 没有 __article(并且可能会抱怨 Comments 也没有 __revision 如果它有远的)。我假设这就是文档真正指的是什么。所以我想我会试试这个:

comments = query._clone().filter(action_object_content_type=comment_ctype).prefetch_related('action_object__article')
articles = query._clone().filter(action_object_content_type=article_ctype).prefetch_related('action_object__revision')
query = comments | articles

但是结果总是空的。我想查询集只支持单个 prefetch_related 列表,不能像那样连接。

我喜欢 return 的单个查询集,因为稍后在这部分不知道的代码中进行进一步过滤。尽管一旦最终评估了查询集,我希望能够让 Django 获取呈现事件所需的一切。

还有其他方法吗?

我看过 Prefetch objects,但我认为他们在这种情况下没有提供任何帮助。

可以在 django-notify-x 中找到一个解决方案,它源自 django-notifications,而后者又源自 django-activity-stream。它使用下面复制的文本中链接的 "django snippet"。

https://github.com/v1k45/django-notify-x/pull/19

Using a snippet from https://djangosnippets.org/snippets/2492/, prefetch generic relations to reduce the number of queries.

Currently, we trigger one additional query for each generic relation for each record, with this code, we reduce to one additional query for each generic relation for each type of generic relation used.

If all your notifications are related to a Badges model, only one aditional query will be triggered.

对于 Django 1.10 和 1.11,我使用上面的代码片段修改如下(以防万一你没有使用 django-activity-stream):

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import fields as generic


def get_field_by_name(meta, fname):
    return [f for f in meta.get_fields() if f.name == fname]


def prefetch_relations(weak_queryset):
    weak_queryset = weak_queryset.select_related()

    # reverse model's generic foreign keys into a dict:
    # { 'field_name': generic.GenericForeignKey instance, ... }
    gfks = {}
    for name, gfk in weak_queryset.model.__dict__.items():
        if not isinstance(gfk, generic.GenericForeignKey):
            continue
        gfks[name] = gfk

    data = {}
    for weak_model in weak_queryset:
        for gfk_name, gfk_field in gfks.items():
            related_content_type_id = getattr(weak_model, get_field_by_name(gfk_field.model._meta, gfk_field.ct_field)[
                0].get_attname())
            if not related_content_type_id:
                continue
            related_content_type = ContentType.objects.get_for_id(related_content_type_id)
            related_object_id = int(getattr(weak_model, gfk_field.fk_field))

            if related_content_type not in data.keys():
                data[related_content_type] = []
            data[related_content_type].append(related_object_id)

    for content_type, object_ids in data.items():
        model_class = content_type.model_class()
        models = prefetch_relations(model_class.objects.filter(pk__in=object_ids))
        for model in models:
            for weak_model in weak_queryset:
                for gfk_name, gfk_field in gfks.items():
                    related_content_type_id = getattr(weak_model,
                                                      get_field_by_name(gfk_field.model._meta, gfk_field.ct_field)[
                                                          0].get_attname())
                    if not related_content_type_id:
                        continue
                    related_content_type = ContentType.objects.get_for_id(related_content_type_id)
                    related_object_id = int(getattr(weak_model, gfk_field.fk_field))

                    if related_object_id != model.pk:
                        continue
                    if related_content_type != content_type:
                        continue

                    setattr(weak_model, gfk_name, model)

    return weak_queryset

这给了我预期的结果。

编辑:

要使用它,您只需调用 prefetch_relations,并将您的 QuerySet 作为参数。

例如,而不是:

my_objects = MyModel.objects.all()

你可以这样做:

my_objects = prefetch_relations(MyModel.objects.all())