【问题标题】:Prefetching model with GenericForeignKey使用 GenericForeignKey 预取模型
【发布时间】:2021-12-08 08:50:50
【问题描述】:

我有一个数据结构,其中Document 有许多Blocks,而其中恰好有一个ParagraphHeader。一个简化的实现:

class Document(models.Model):
  title = models.CharField()

class Block(models.Model):
  document = models.ForeignKey(to=Document)
  content_block_type = models.ForeignKey(to=ContentType)
  content_block_id = models.CharField()
  content_block = GenericForeignKey(
    ct_field="content_block_type",
    fk_field="content_block_id",
  )

class Paragraph(models.Model):
  text = models.TextField()

class Header(models.Model):
  text = models.TextField()
  level = models.SmallPositiveIntegerField()

(请注意,与上面的实现不同,ParagraphHeader 确实需要在不同的模型中。)

我使用jinja2 为文档制作一个 Latex 文件模板。尽管 jinja 为每个块和段落或标题执行新的数据库查询,但模板化速度很慢。

template = get_template(template_name="latex_templates/document.tex", using="tex")
return template.render(context={'script': self.script})
\documentclass[a4paper,10pt]{report}
\begin{document}
  {% for block in chapter.block_set.all() %}
    {% if block.content_block_type.name == 'header' %}
      \section{ {{- block.content_block.latex_text -}} }
    {% elif block.content_block_type.name == 'paragraph' %}
      {{ block.content_block.latex_text }}
    {% endif %}
  {% endfor %}
\end{document}

content_block.latex_text()是一个将HTML字符串转换为Latex字符串的函数)

因此我想预取script.blocksblocks.content_block。我知道在 Django 中有两种预取方法:

  1. select_related() 执行JOIN 查询,但仅适用于ForeignKeys。它适用于script.blocks,但不适用于blocks.content_block

  2. prefetch_related() 也适用于 GenericForeignKeys,但如果我正确理解文档,它一次只能获取一个 ContentType,而我有两个。

有没有办法在这里执行必要的预取?感谢您的帮助。

【问题讨论】:

  • 我认为Reverse generic relations 可能会有所帮助。您可以在ParagraphHeader 中定义这些,并为两者添加单独的预取
  • 感谢您的评论。我确实看到了反向泛型关系如何提供帮助,但是多个预取甚至看起来像一个查询呢?
  • 如果可能的话,你能否分享一下你是如何渲染script的?
  • 感谢您没有放弃这一点。我在上面添加了模板。
  • 如何将多个预取视为查询? - 预取是在 Python 中完成的,而不是通过 JOIN。所以它会触发多个查询,一个用于主模型,一个用于预取模型

标签: django django-models django-templates django-queryset django-generic-relations


【解决方案1】:

我的错,我没有注意到文档是一个 FK,并且反向 FK 不能与 select_related 连接。

首先,我还是建议添加related_name="blocks"

当你预取时,你可以传递查询集。但是你不应该通过doc_id的过滤器,Django的ORM会自动添加它。

如果你传递了查询集,你还可以在那里添加选择/预取相关的调用。

blocks_qs = Block.objects.all().prefetch_related('content_block')
doc_prefetched = Document.objects.prefetch_related(
    Prefetch('blocks', queryset=blocks_qs)
  ).get(uuid=doc_uuid)

但如果您不需要额外的过滤器或注释,则更简单的语法可能适合您

document = (
 Document.objects
  .prefecth_related('blocks', 'blocks__content_block')
  .get(uuid=doc_uuid)
)

【讨论】:

  • 感谢您的回答。我认为这不起作用,因为blocks 不是Document 的属性,而是相反。我想这就是为什么查询抱怨Invalid field name(s) given in select_related: 'blocks'. 我尝试用Block.document = models.ForeignKey(to=Document, related_name="blocks") 解决这个问题,但无济于事。当我将模板化字符串传递给 Latex 引擎时,没有视图。
  • 从您的回答中思考,这可行:doc_prefetched = Document.objects.prefetch_related(Prefetch('blocks', queryset=Block.objects.filter(document_id=doc.uuid))).get(uuid=doc.uuid)。问题是如何预取 content_blocks...
  • @nehalem 我用 prefetch_related 用法更新了答案。
【解决方案2】:

这不是一个优雅的解决方案,但您可以尝试使用reverse generic relations

from django.contrib.contenttypes.fields import GenericRelation


class Paragraph(models.Model):
  text = models.TextField()
  blocks = GenericRelation(Block, related_query_name='paragraph')

class Header(models.Model):
  text = models.TextField()
  level = models.SmallPositiveIntegerField()
  blocks = GenericRelation(Block, related_query_name='header')

并对此进行预取:

Document.objects.prefetch_related('block_set__header', 'block_set__paragraph')

然后将模板渲染更改为类似(未测试,稍后将尝试测试):

\documentclass[a4paper,10pt]{report}
\begin{document}
  {% for block in chapter.block_set.all %}
    {% if block.header %}
      \section{ {{- block.header.0.latex_text -}} }
    {% elif block.paragraph %}
      {{ block.paragraph.0.latex_text }}
    {% endif %}
  {% endfor %}
\end{document}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-09-10
    • 2021-09-30
    • 2012-01-28
    • 2019-08-11
    • 2014-07-12
    • 2015-01-18
    • 1970-01-01
    • 2016-04-07
    相关资源
    最近更新 更多