'Does Django cache related ForeignKey and ManyToManyField fields once they're accessed?

Given the following model, does Django cache the related objects after the first time they're accessed?

class Post(models.Model):
    authors = models.ManyToManyField(User)
    category = models.ForeignKey(Category)

For example:

post = Post.objects.get(id=1)

# as i understand this hits the database
authors1 = post.authors.all()
# does this his the database again?
authors2 = post.authors.all()

# as i understand this hits the database
category1 = post.category
# does this hit the database again?
category2 = post.category

Note: currently working with Django 1.3, but it's good to know what's available in other versions.



Solution 1:[1]

In the first example the second query is cached. In the second case (I believe) they will both cause a DB hit unless you use select_related on the original query:

post = Post.objects.select_related('category').get(id=1)

EDIT

I'm wrong about the second example. If you use select_related in the original query, you won't hit the database at all again (The ForeignKey is cached immediately). If you don't use select_related, you will hit the DB on the first query but the second query will be cached.

From:

https://docs.djangoproject.com/en/dev/topics/db/queries/#one-to-many-relationships

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.

Note that the select_related() QuerySet method recursively prepopulates the cache of all one-to-many relationships ahead of time.

Solution 2:[2]

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)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2