使用 GenericForeignKey 预取模型

Posted

技术标签:

【中文标题】使用 GenericForeignKey 预取模型【英文标题】:Prefetching model with 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
\begindocument
  % 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 %
\enddocument

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

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

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

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

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

【问题讨论】:

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

【讨论】:

以上是关于使用 GenericForeignKey 预取模型的主要内容,如果未能解决你的问题,请参考以下文章

如何使用另一个查询作为可能值通过 GenericForeignKey-Field 过滤查询?

无法在 django 1.8.4 中使用 GenericForeignKey

Django预取相关并存在

通过预取提高性能并选择相关

python 用于GenericForeignKey字段的django管理过滤器

Tensorflow 数据集预取和缓存选项的正确用途是啥?