【问题标题】:Django prefetch related no duplicates with intermediate tableDjango prefetch 与中间表无关
【发布时间】:2025-12-06 21:45:02
【问题描述】:

我有一个问题想解决一天。

模型

class Quote(models.Model):
    text = models.TextField()
    source = models.ForeignKey(Source)
    tags = models.ManyToManyField(Tag)
    ...

class Source(models.Model):
    title = models.CharField(max_length=100)
    ...

class Tag(models.Model):
    name = models.CharField(max_length=30,unique=True)
    slug = models.SlugField(max_length=40,unique=True)
    ...

我正在尝试为引号世界建模。有关系:一个Source 有很多Quotes,一个Quote 有很多Tags。 问题是:

  1. 如何获取包含在Source 中的所有Tags(通过包含的Quotes)?
  2. 尽可能少的查询。
  3. 它们包含在该来源中的次数

我用模型方法尝试过与预取无关的简单方法

def source_tags(self):
    tags = Tag.objects.filter(quote__source__id=self.id).distinct().annotate(usage_count=Count('quote'))
    return sorted(tags, key=lambda tag:-tag.usage_count)

在模板中:

{% for tag in source.source_tags|slice:":5" %}
    source.quote
{% endfor %}

现在我有

sources = Source.objects.all().prefetch_related('quote_set__tags')

在模板中,我不知道如何正确迭代以获取一个来源的 Tags,以及如何计算它们而不是列出重复的标签。

【问题讨论】:

    标签: django django-orm


    【解决方案1】:

    这将在单个 SQL 查询中得到结果:

    # views.py
    from django.db.models import Count
    from .models import Source
    
    
    def get_tag_count():
        """
        Returns the count of tags associated with each source
        """
        sources = Source.objects.annotate(tag_count=Count('quote__tags')) \
                             .values('title', 'quote__tags__name', 'tag_count') \
                             .order_by('title')
        # Groupe the results as
        # {source: {tag: count}}
        grouped = {}
        for source in sources:
            title = source['title']
            tag = source['quote__tags__name']
            count = source['tag_count']
            if not title in grouped:
                grouped[title] = {}
            grouped[title][tag] = count
        return grouped
    
    
    
    # in template.html
    
    {% for source, tags in sources.items %}
    
        <h3>{{ source }}</h3>
    
        {% for tag, count in tags.items %}
            {% if tag %}
                <p>{{ tag }} : {{ count }}</p>
            {% endif %}
        {% endfor %}
    
    {% endfor %}
    

    补充测试:)

    # tests.py
    from django.test import TestCase
    from .models import Source, Tag, Quote
    from .views import get_tag_count
    
    
    class SourceTags(TestCase):
    
        def setUp(self):
            abc = Source.objects.create(title='ABC')
            xyz = Source.objects.create(title='XYZ')
    
            inspire = Tag.objects.create(name='Inspire', slug='inspire')
            lol = Tag.objects.create(name='lol', slug='lol')
    
            q1 = Quote.objects.create(text='I am inspired foo', source=abc)
            q2 = Quote.objects.create(text='I am inspired bar', source=abc)
            q3 = Quote.objects.create(text='I am lol bar', source=abc)
            q1.tags = [inspire]
            q2.tags = [inspire]
            q3.tags = [inspire, lol]
            q1.save(), q2.save(), q3.save()
    
        def test_count(self):
            # Ensure that only 1 SQL query is done
            with self.assertNumQueries(1):
                sources = get_tag_count()
                self.assertEqual(sources['ABC']['Inspire'], 3)
                self.assertEqual(sources['ABC']['lol'], 1)
    

    我基本上使用了 ORM 中的 annotatevalues 函数。它们非常强大,因为它们会自动执行连接。它们也非常高效,因为它们只访问数据库一次,并且只返回那些指定的字段。

    【讨论】:

    • 我无法按预期方式工作。值列表多次包含每个源值...(至少 {% for source in sources %} 不起作用..),结果不再按来源分组
    • 对于每个标签,值列表中都有一个条目。我认为没有 Q 对象或预取相关是不容易的。
    • 也许最好的方法是只缓存结果,因为这似乎是一项艰巨的任务......现在尝试获取一个 mysql 查询,我可以在 * 的帮助下将其转换为 django orm 查询
    • 我已经更新了上面的视图和测试以返回字典 {source: {tag: count}} 中的标签计数。因此每个来源只会出现一次。
    • 仍未按预期工作。因为每个源对象中没有标签列表
    最近更新 更多