【问题标题】:Django prefetch_related GenericForeignKey with multiple content types具有多种内容类型的 Django prefetch_related GenericForeignKey
【发布时间】:2016-02-22 15:44:13
【问题描述】:

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

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

问题是这会导致对每个项目的查询,特别是Comment.get_absolute_url 需要评论的article,它还没有被获取,Article.__unicode__ 需要它的revision.content,它也没有被获取.

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

它还支持预取 GenericRelation 和 GenericForeignKey,但是,它必须限制为一组同质的结果。例如,仅当查询仅限于一种 ContentType 时,才支持预取由 GenericForeignKey 引用的对象。

而且内容类型不止一种。但是在我上面的用例中,我需要额外的 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 列表,不能像那样加入。

我喜欢返回单个查询集,因为稍后会在这部分不知道的代码中完成进一步的过滤。尽管最终评估查询集后,我希望能够让 django 获取呈现事件所需的所有内容。

还有其他方法吗?

我查看了Prefetch objects,但我认为他们在这种情况下不会提供任何帮助。

【问题讨论】:

    标签: python django django-queryset django-orm


    【解决方案1】:

    可以在django-notify-x 中找到解决方案,该解决方案源自django-notifications,而django-notifications 又源自django-activity-stream。它使用下面复制文本中链接的“django sn-p”。

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

    使用来自https://djangosnippets.org/snippets/2492/ 的 sn-p, 预取通用关系以减少查询次数。

    目前,我们为每个通用关系触发一个额外的查询 对于每条记录,使用此代码,我们减少了一个额外的查询 所使用的每种泛型关系的每个泛型关系。

    如果您的所有通知都与徽章模型相关,则只有一个 将触发附加查询。

    【讨论】:

      【解决方案2】:

      对于 Django 1.10 和 1.11,我使用上面的 sn-p 修改如下(以防万一你没有使用 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())
      

      【讨论】:

      • 您好!您可以添加评论您的代码到底在做什么吗?如果我想使用此代码,我需要修改什么?假设我有模型Article。我需要查看该模式中的最后一个事件。喜欢编辑/创建,有人添加评论。
      • @NurzhanNogerbek 我的代码与接受的答案中提供的代码几乎相同。但我修改它以使用 Django 1.11。这通过简单地将您的查询集作为参数传递给 prefetch_relations 方法来使用。有关示例,请参见编辑。
      • 在我的情况下,我按原样使用代码,它应该是通用的。
      • 据我所知,它对django-notify-x 应用程序的改进。我使用 Django 1.10。所以我认为你的代码对我有帮助。我是偷不undertand一个认为。此行my_objects = prefetch_relations(MyModel.objects.all()) 将显示 MyModal 中的最后事件?我是对的?
      • 如果您使用:Event.objects.all() 获取事件,那么prefetch_relations(Event.objects.all()) 将为您提供具有预取关系的事件。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-01-22
      • 1970-01-01
      • 2020-07-12
      • 2014-06-08
      • 2012-08-13
      • 1970-01-01
      • 2011-03-12
      相关资源
      最近更新 更多