Django在访问后会缓存相关的ForeignKey和ManyToManyField字段吗?

16 投票
2 回答
6379 浏览
提问于 2025-04-17 07:20

假设有一个这样的模型,Django在第一次访问相关对象后,会把这些对象缓存起来吗?

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

举个例子:

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

注意:目前我在使用Django 1.3,但了解其他版本的情况也是很有帮助的。

2 个回答

0

Django在第一次访问ForeignKey字段后会把它缓存起来,这样下次再用的时候就不用再去数据库查了。不过,ManyToMany字段就不会被缓存,正如Daniel Roseman在其他回答的评论中提到的。这一点在Django的文档中也有提到,关于数据库优化的部分(虽然没有特别提到ManyToMany)

不过一般来说,可调用的属性每次都会导致数据库查询:

我通过以下测试确认了这一点,测试是在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)
9

在第一个例子中,第二个查询是被缓存的在第二种情况下(我认为)如果不使用 select_related,两个查询都会访问数据库:

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

编辑

我之前关于第二个例子的理解是错的。如果在原始查询中使用了 select_related,那么就不会再访问数据库了(外键会立即被缓存)。如果不使用 select_related,第一次查询会访问数据库,但第二次查询会被缓存。

来源:

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

第一次访问一对多关系时,相关对象的访问会被缓存。之后对同一个对象实例的外键的访问也会被缓存。

需要注意的是,select_related() 这个查询集方法会提前递归地填充所有一对多关系的缓存。

撰写回答