Prefetching model with GenericForeignKey

231 views Asked by At

I have a data structure in which a Document has many Blocks which have exactly one Paragraph or Header. A simplified implementation:

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

(Note that there is an actual need for having Paragraph and Header in separate models unlike in the implementation above.)

I use jinja2 to template a Latex file for the document. Templating is slow though as jinja performs a new database query for every Block and Paragraph or Header.

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() is a function that converts a HTML string to a Latex string)

Hence I would like to prefetch script.blocks and blocks.content_block. I understand that there are two methods for prefetching in Django:

  1. select_related() performs a JOIN query but only works on ForeignKeys. It would work for script.blocks but not for blocks.content_block.

  2. prefetch_related() works with GenericForeignKeys as well, but if I understand the docs correctly, it can only fetch one ContentType at a time while I have two.

Is there any way to perform the necessary prefetching here? Thank you for your help.

2

There are 2 answers

3
Igor On

My bad, I did not notice that document is an FK, and reverse FK can not be joined with select_related.

First of all, I would suggest to add related_name="blocks" anyway.

When you prefetch, you can pass the queryset. But you should not pass filters by doc_id, Django's ORM adds it automatically.

And if you pass the queryset, you can also add select/prefetch related call there.

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

But if you don't need extra filters or annotation, the simpler syntax would probably work for you

document = (
 Document.objects
  .prefecth_related('blocks', 'blocks__content_block')
  .get(uuid=doc_uuid)
)
0
Brian Destura On

Not really an elegant solution but you can try using 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')

and prefetch on that:

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

then change the template rendering to something like (not tested, will try to test later):

\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}