After you've done the prefetch, it seems like the only cheap way to access the child records is through all(). Any filters seem to trigger another database query.
The short answer to your question about all the paragraphs in a book is to use a list comprehension with two levels:
paragraphs = [paragraph
for page in book.page_set.all()
for paragraph in page.paragraph_set.all()]
Here's a runnable example:
# Tested with Django 1.11.13
from __future__ import print_function
import os
import sys
import django
from django.apps import apps
from django.apps.config import AppConfig
from django.conf import settings
from django.core.files.base import ContentFile, File
from django.db import connections, models, DEFAULT_DB_ALIAS
from django.db.models.base import ModelBase
from django_mock_queries.mocks import MockSet, mocked_relations
NAME = 'udjango'
def main():
setup()
class Book(models.Model):
name = models.CharField(max_length=64)
class Page(models.Model):
number = models.IntegerField()
book = models.ForeignKey(Book)
class Paragraph(models.Model):
number = models.IntegerField()
page = models.ForeignKey(Page)
syncdb(Book)
syncdb(Page)
syncdb(Paragraph)
b = Book.objects.create(name='Gone With The Wind')
p = b.page_set.create(number=1)
p.paragraph_set.create(number=1)
b = Book.objects.create(name='The Three Body Problem')
p = b.page_set.create(number=1)
p.paragraph_set.create(number=1)
p.paragraph_set.create(number=2)
p = b.page_set.create(number=2)
p.paragraph_set.create(number=1)
p.paragraph_set.create(number=2)
books = Book.objects.all().prefetch_related('page_set',
'page_set__paragraph_set')
for book in books:
print(book.name)
paragraphs = [paragraph
for page in book.page_set.all()
for paragraph in page.paragraph_set.all()]
for paragraph in paragraphs:
print(paragraph.page.number, paragraph.number)
def setup():
DB_FILE = NAME + '.db'
with open(DB_FILE, 'w'):
pass # wipe the database
settings.configure(
DEBUG=True,
DATABASES={
DEFAULT_DB_ALIAS: {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': DB_FILE}},
LOGGING={'version': 1,
'disable_existing_loggers': False,
'formatters': {
'debug': {
'format': '%(asctime)s[%(levelname)s]'
'%(name)s.%(funcName)s(): %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S'}},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'debug'}},
'root': {
'handlers': ['console'],
'level': 'WARN'},
'loggers': {
"django.db": {"level": "DEBUG"}}})
app_config = AppConfig(NAME, sys.modules['__main__'])
apps.populate([app_config])
django.setup()
original_new_func = ModelBase.__new__
@staticmethod
def patched_new(cls, name, bases, attrs):
if 'Meta' not in attrs:
class Meta:
app_label = NAME
attrs['Meta'] = Meta
return original_new_func(cls, name, bases, attrs)
ModelBase.__new__ = patched_new
def syncdb(model):
""" Standard syncdb expects models to be in reliable locations.
Based on https://github.com/django/django/blob/1.9.3
/django/core/management/commands/migrate.py#L285
"""
connection = connections[DEFAULT_DB_ALIAS]
with connection.schema_editor() as editor:
editor.create_model(model)
main()
And here's the end of the output. You can see that it only runs one query for each table.
2018-10-30 15:58:25[DEBUG]django.db.backends.execute(): (0.000) SELECT "udjango_book"."id", "udjango_book"."name" FROM "udjango_book"; args=()
2018-10-30 15:58:25[DEBUG]django.db.backends.execute(): (0.000) SELECT "udjango_page"."id", "udjango_page"."number", "udjango_page"."book_id" FROM "udjango_page" WHERE "udjango_page"."book_id" IN (1, 2); args=(1, 2)
2018-10-30 15:58:25[DEBUG]django.db.backends.execute(): (0.000) SELECT "udjango_paragraph"."id", "udjango_paragraph"."number", "udjango_paragraph"."page_id" FROM "udjango_paragraph" WHERE "udjango_paragraph"."page_id" IN (1, 2, 3); args=(1, 2, 3)
Gone With The Wind
1 1
The Three Body Problem
1 1
1 2
2 1
2 2