1

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.

Brian Destura
  • 11,487
  • 3
  • 18
  • 34
nehalem
  • 397
  • 2
  • 20
  • I think [`Reverse generic relations`](https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#reverse-generic-relations) might help. You can define these in `Paragraph` and `Header`, and add separate prefetch for both – Brian Destura Oct 21 '21 at 22:27
  • Thank you for your comment. I do see how Reverse generic relations could help, but how would multiple prefetches even look as a query? – nehalem Oct 23 '21 at 16:20
  • If possible can you also share how you render `script`? – Brian Destura Oct 26 '21 at 21:51
  • Thank you for not giving up on this. I added the template above. – nehalem Oct 27 '21 at 07:16
  • how would multiple prefetches even look as a query? - Prefetch is done in the Python, not via JOIN. So it will fire multiple queries, one for main model and then one for the model being prefetched – Igor Oct 27 '21 at 10:44
  • This is tricky because of the constraint on having homogenous results when prefetching `content_block` :( – Brian Destura Oct 28 '21 at 04:25

2 Answers2

0

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)
)
Igor
  • 3,129
  • 1
  • 21
  • 32
  • Thank you for your answer. I don't think this works as `blocks` is not an attribute on `Document` but the other way around. I guess that's why the query complains `Invalid field name(s) given in select_related: 'blocks'.` I tried fixing this with a `Block.document = models.ForeignKey(to=Document, related_name="blocks")` but to no avail. There is no view as I pass the templated string to a Latex engine. – nehalem Oct 27 '21 at 15:06
  • Thinking from your answer, this works though: `doc_prefetched = Document.objects.prefetch_related(Prefetch('blocks', queryset=Block.objects.filter(document_id=doc.uuid))).get(uuid=doc.uuid)`. Question is how to prefetch the content_blocks… – nehalem Oct 27 '21 at 15:32
  • @nehalem I updated the answer with prefetch_related usage. – Igor Oct 29 '21 at 07:46
0

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}
Brian Destura
  • 11,487
  • 3
  • 18
  • 34