使用 GenericForeignKey 预取模型
Posted
技术标签:
【中文标题】使用 GenericForeignKey 预取模型【英文标题】:Prefetching model with GenericForeignKey 【发布时间】:2021-12-08 08:50:50 【问题描述】:我有一个数据结构,其中Document
有许多Blocks
,而其中恰好有一个Paragraph
或Header
。一个简化的实现:
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()
(请注意,与上面的实现不同,Paragraph
和 Header
确实需要在不同的模型中。)
我使用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.blocks
和blocks.content_block
。我知道在 Django 中有两种预取方法:
select_related()
执行JOIN
查询,但仅适用于ForeignKeys
。它适用于script.blocks
,但不适用于blocks.content_block
。
prefetch_related()
也适用于 GenericForeignKeys,但如果我正确理解文档,它一次只能获取一个 ContentType
,而我有两个。
有没有办法在这里执行必要的预取?感谢您的帮助。
【问题讨论】:
我认为Reverse generic relations
可能会有所帮助。您可以在Paragraph
和Header
中定义这些,并为两者添加单独的预取
感谢您的评论。我确实看到了反向泛型关系如何提供帮助,但是多个预取甚至看起来像一个查询呢?
如果可能的话,你能否分享一下你是如何渲染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