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

时间:2015-11-20 08:11:53

标签: python django django-queryset django-orm

我正在使用django-activity-stream来显示最近发生的事件列表。为了举例,这些可能是有人评论或有人编辑文章。即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{{ action.action_object.id }},但action_object_content_type prefetch_related只会对query = query.prefetch_related('action_object__article`, `action_object__revision`) 进行单一查询,但这似乎正在进行测试。

  

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

并且有多种内容类型。但是在上面的用例中,我需要额外的Article次调用,例如:

__article

但是这会抱怨,因为Comment没有__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也没有projects' for nil:NilClass):
app/controllers/project_controller.rb:8:in
。我假设这是文档真正指的是什么。所以我想我会试试这个:

before_action :confirm_logged_in

  before_action :find_company


  def index

    @projects = @company.projects.sorted
  end

  def show

    @project = Project.find(params[:id])
  end

  def new

    @project = Project.new()
  end

  def create

    @project = Project.new(project_params)
    if @project.save
      redirect_to(:action => 'index')
    else
      render('new')
    end
  end

  def edit

    @project = Project.find(params[:id])
  end

  def update

    @project = Project.find(params[:id])
    if @project.update_attributes(project_params)
      redirect_to(:action => 'index')
    else
      render('new')
    end
  end

  def delete

    @project = Project.find(params[:id])
  end

  def destory

    @project = Project.find(params[:id])
    @project.destroy
    redirect_to(:action => 'index')
  end


  private 

  def project_params

   params.require(:project).permit(:name, :position, :type_of_project, :description, :no_of_tasks)
  end

  def find_company

    if params[:company_id]
      @company = Company.find(params[:company_id])
    end
  end
end

但结果总是空的。我猜查询集只支持单个CurrentSheet.Cells(PRow, "AD").FormulaR1C1 = "=CONCATENATE(YEAR(R[0]C[-19]), ""/"",TEXT(MONTH(R[0]C[-19]),""MM""))" 列表,不能像那样加入。

我喜欢返回一个查询集,因为稍后会在代码中进行进一步的过滤,而这部分不知道。虽然一旦查询集最终被评估,我希望能够让django获取呈现事件所需的一切。

还有其他办法吗?

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

2 个答案:

答案 0 :(得分:1)

可以在django-notify-x中找到一个解决方案,该解决方案来自django-notifications,而django-activity-stream来自li。它使用了一个&#34; django片段&#34;链接在下面的复制文本中。

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

  

使用https://djangosnippets.org/snippets/2492/中的代码段,   预取通用关系以减少查询次数。

     

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

     

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

答案 1 :(得分:1)

对于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())