16

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.

bpscott
  • 484
  • 3
  • 15

2 Answers2

9

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.

Timmy O'Mahony
  • 53,000
  • 18
  • 155
  • 177
  • 2
    Still not quite right. ManyToMany queries are not cached at all - they are really just equivalent to reverse FK lookups, so do not get cached except when using the new `prefetch_related` functionality in 1.4. – Daniel Roseman Nov 29 '11 at 19:39
  • Thanks. I'll handle caching on ManyToMany fields myself when necessary. – bpscott Nov 30 '11 at 15:14
  • Answer needs to be fixed and better yet tested. I don't see caching in either case in DJ 1.5. – Bryce May 16 '13 at 19:54
0

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)