Django seems to cache ForeignKey
fields after they are first accessed, but will not cache ManyToMany
, as mentioned in the comments to the other answer by Daniel Roseman. This is also mentioned in the Django docs about DB optimizations (although it doesn't explicitly call out ManyToMany)
But in general, callable attributes cause DB lookups every time:
I confirmed this with the following test, executed against Django 3.2.8
from django.db.models import prefetch_related_objects
from django.test import TestCase
from django.db import connection
from django.test.utils import CaptureQueriesContext
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Category(models.Model):
category_name = models.CharField(max_length=100)
class Post(models.Model):
name = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
class TestQueryCache(TestCase):
def _create_objects(self):
author1 = Author(name="first author")
author1.save()
author2 = Author(name="second author")
author2.save()
category1 = Category(category_name="first category")
category1.save()
category2 = Category(category_name="second category")
category2.save()
post = Post.objects.create(name="test post", category=category1)
post.authors.add(author1, author2)
post.save()
return post
def test_foreign_key(self):
self._create_objects()
post = Post.objects.get(name="test post")
with CaptureQueriesContext(connection) as ctx:
category1 = post.category.category_name
category2 = post.category.category_name
print("ForeignKey queries are")
for query in ctx.captured_queries:
print(query)
# this call pattern makes a single query, since foreign keys are cached
# https://docs.djangoproject.com/en/dev/topics/db/queries/#forward
# "Forward access to one-to-many relationships is cached the first time the related object is accessed. Subsequent accesses to the foreign key on the same object instance are cached."
# SELECT "coaching_category"."id", "coaching_category"."category_name" FROM "coaching_category" WHERE "coaching_category"."id" = 1 LIMIT 21
def test_many_to_many(self):
self._create_objects()
post = Post.objects.get(name="test post")
with CaptureQueriesContext(connection) as ctx:
authors1 = [author.name for author in post.authors.all()]
authors2 = [author.name for author in post.authors.all()]
print("Without prefetching, ManyToMany queries are")
for query in ctx.captured_queries:
print(query)
# This call pattern makes two queries, it seems that unlike ForeignKey, ManyToMany relationships are not cached
# SELECT "coaching_author"."id", "coaching_author"."name" FROM "coaching_author" INNER JOIN "coaching_post_authors" ON ("coaching_author"."id" = "coaching_post_authors"."author_id") WHERE "coaching_post_authors"."post_id" = 2
# SELECT "coaching_author"."id", "coaching_author"."name" FROM "coaching_author" INNER JOIN "coaching_post_authors" ON ("coaching_author"."id" = "coaching_post_authors"."author_id") WHERE "coaching_post_authors"."post_id" = 2
def test_many_to_many_prefetching(self):
self._create_objects()
post = Post.objects.get(name="test post")
with CaptureQueriesContext(connection) as ctx:
prefetch_related_objects([post], "authors")
# as i understand this hits the database
authors1 = [author.name for author in post.authors.all()]
# does this his the database again?
authors2 = [author.name for author in post.authors.all()]
print("With prefetching, ManyToMany queries are")
for query in ctx.captured_queries:
print(query)
# using prefetch allows ManyToMany to be cached, this call pattern makes only a single query
# SELECT ("coaching_post_authors"."post_id") AS "_prefetch_related_val_post_id", "coaching_author"."id", "coaching_author"."name" FROM "coaching_author" INNER JOIN "coaching_post_authors" ON ("coaching_author"."id" = "coaching_post_authors"."author_id") WHERE "coaching_post_authors"."post_id" IN (3)