Django cached_property没有被缓存

v64noz0r  于 2023-06-25  发布在  Go
关注(0)|答案(2)|浏览(135)

我的模型中有以下内容:

class Tag(models.Model):
    name = models.CharField(max_length=255)
    type = models.CharField(max_length=1)
    person = models.ForeignKey(People, on_delete=models.CASCADE)

class People(models.Model):
    name = models.CharField(max_length=255)

    @cached_property
    def tags(self):
        return Tag.objects.filter(person=self, type="A")

我希望当我这样做时:

person = People.objects.get(pk=1)
tags = person.tags

这将导致1 db查询-只从数据库中获取人。然而,它会持续导致2个查询-标签表被一致地查询,即使这是应该缓存的。什么会导致这种情况?我没有正确使用cached_property吗?
为了说明这种情况,对模型进行了简化。

vaqhlq81

vaqhlq811#

修饰的tags()方法返回一个尚未计算的 queryset。(在Django的文档中阅读更多关于何时计算查询集的信息)。要缓存查询的结果,你必须首先强制查询集计算为一个对象列表:

class People(models.Model):
    name = models.CharField(max_length=255)

    @cached_property
    def tags(self):
        return list(Tag.objects.filter(person=self, type="A"))
5f0d552i

5f0d552i2#

如果不查看多次实际调用缓存属性的代码,很难找出问题所在。但是,从您描述问题的方式来看,cached_property似乎是正确的方法,应该可以工作。
我猜可能是对它的工作原理有一些误解。缓存属性的一个示例用例是:

person = People.objects.get(pk=1)  # <- Query on People executed
text_tags = ', '.join(person.tags)  # <- Query on Tags executed
html_tags = format_html_join(
    '\n',
    '<span class="tag tag-{}">{}</span>',
    ((t.type, t.name) for t in person.tags),  # <- tags loaded from cache, no query executed
)

但是,如果你这样做:

for person in People.objects.all(): # <- Query on People executed
    text_tags = ', '.join(person.tags)  # <- Query on Tags executed FOR EACH ITERATION
    html_tags = format_html_join(
        '\n',
        '<span class="tag tag-{}">{}</span>',
        ((t.type, t.name) for t in person.tags),  # <- tags loaded from cache, no query executed
    )

for循环每次迭代的第一次调用person.tags执行一个查询。这是因为tags属性的结果是每个示例缓存的。
如果您想在遍历people对象时提前缓存所需的所有标记,根据您的用例,有几种方法。

手动方法

from itertools import groupby

all_tags = Tags.objects.filter(type="A").order_by('person_id')
# order_by() is important because we will use person_id as key to group the results using itertools.groupby()

# Create a dictionary matching a person to its list of tags using a single SQL query
people_tags = {
    person_id: list(tags)
    for person_id, tags in groupby(all_tags, lambda t: t.person_id)
}

for person in People.objects.all():
    # Try to find the person's tags in the dictionary, otherwise, set tags to an empty list
    tags = people_tags.get(person.id, [])

聚合方式的单查询

对于这种方法,您需要确保您的外键具有相关的名称,以便能够进行“反向”查询:

class Tag(models.Model):
    name = models.CharField(max_length=255)
    type = models.CharField(max_length=1)
    person = models.ForeignKey(
        People,
        on_delete=models.CASCADE,
        related_name='tags',
    )

指定related_name并不是严格要求的,因为Django给出了一个默认的相关名称,但我不记得这个名称是如何构建的,所以我总是明确地给出它。

不要忘记删除tags()方法,因为名称会与相关名称“tags”冲突。

from django.db.models import Q
from django.contrib.postgres.aggregates import ArrayAgg

persons = (
    People.objects.all()
    .annotate(tags_names=ArrayAgg('tags__name', filter=Q(tags__type='A')))
)
for person in persons:
    tags = person.tags_names

注意,使用这种方法,person.tags_names将是一个字符串形式的标记名称列表,而不是Tag对象列表。使用annotate()有一些很棘手的方法来检索Tag对象,或者至少是多个字段,但我认为这超出了这个问题的范围。
另外请注意,这只适用于PostgreSQL。

Django的内置方式:prefetch_related()

Django在QuerySet对象上提供了prefetch_related()方法。它被特别设计为手动方法的捷径。这种方法需要使用上面提到的外键related_name

from django.db.models import Prefetch

persons = (
    People.objects.all()
    .prefetch_related(
        Prefetch('tags', queryset=Tag.objects.filter(type='A'))
    )
)
for person in persons:
    tags = person.tags

注意,如果不需要按类型过滤标记,可以简单地执行People.objects.prefetch_related('tags')

相关问题