You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Django ORM高效查询嵌套多对多表,获取带食谱的烹饪步骤

问题描述

我设计了如下Django烹饪会话相关模型:

from django.db import models

class Recipe(models.Model):
    name = models.CharField(max_length=255)
    
    def __str__(self):
        return self.name

class Step(models.Model):
    name = models.CharField(max_length=255)
    recipes = models.ManyToManyField(Recipe, related_name='steps')

    def __str__(self):
        return self.name

class CookingSession(models.Model):
    name = models.CharField(max_length=255)
    steps = models.ManyToManyField(Step, related_name='cooking_sessions')

    def __str__(self):
        return self.name

当前使用如下查询:

cooking_sessions = (
            CookingSession.objects.annotate(
             
                step_list=ArrayAgg(
                    models.F(
                        "steps__name",
                    ),
                    distinct=True,
                ),
                recipe_list=ArrayAgg(models.F("steps__recipes__name")),
                
            )
           
        )

得到的数据格式为:

[
    {
        'id': 1,
        'name': 'Italian Night',
        'step_list': ['Preparation', 'Cooking', 'Serving'],
        'recipe_list': ['Tomato Sauce', 'Pasta Dough', 'Spaghetti', 'Tomato Sauce', 'Garlic Bread']
    },
    ...
]

希望以最少的查询次数(最好仅1次)获取特定烹饪会话的所有步骤,且每个步骤包含对应的食谱,期望的数据格式如下:

{
    'id': 1,
    'name': 'Italian Night',
    'steps': [
        {
            'step_name': 'Preparation',
            'recipes': ['Tomato Sauce', 'Pasta Dough']
        },
        {
            'step_name': 'Cooking',
            'recipes': ['Spaghetti', 'Tomato Sauce']
        },
        {
            'step_name': 'Serving',
            'recipes': ['Garlic Bread']
        }
    ]
}

请问该如何实现?

解决方案

可以通过两种方式实现单次查询获取目标格式数据,以下是具体方案:

方案1:纯ORM聚合实现(单次数据库查询)

利用嵌套ArrayAgg和Subquery,为每个步骤聚合对应的食谱列表,再将结构化的步骤数据聚合到烹饪会话中:

from django.db import models
from django.db.models import Subquery, OuterRef, ArrayAgg

# 子查询:为每个步骤生成对应的食谱名称列表
step_recipes_subquery = Step.objects.filter(
    id=OuterRef('steps__id')
).annotate(
    recipe_names=ArrayAgg('recipes__name', distinct=True)
).values('name', 'recipe_names')

# 查询特定烹饪会话,聚合步骤及对应食谱
target_session = CookingSession.objects.filter(id=1).annotate(
    steps=ArrayAgg(
        models.Value(
            {
                'step_name': models.F('steps__name'),
                'recipes': Subquery(step_recipes_subquery.values('recipe_names')[:1])
            },
            output_field=models.JSONField()
        ),
        distinct=True
    )
).values('id', 'name', 'steps').first()

该方案全程仅1次数据库查询,直接返回符合要求的结构化数据,需要Django 3.1+版本支持JSONField和嵌套聚合语法。

方案2:Prefetch预加载+Python整理(单次数据库查询)

如果觉得嵌套聚合写法复杂,可通过Prefetch预加载关联数据,再在Python层面整理成目标格式:

from django.db.models import Prefetch

# 预加载步骤时同时预加载关联的食谱,避免N+1查询
session = CookingSession.objects.filter(id=1).prefetch_related(
    Prefetch('steps', queryset=Step.objects.prefetch_related('recipes'))
).first()

# 整理成目标格式
result = {
    'id': session.id,
    'name': session.name,
    'steps': [
        {
            'step_name': step.name,
            'recipes': [r.name for r in step.recipes.all()]
        } for step in session.steps.all()
    ]
}

这种方式数据库仅执行1次查询,Python层面的整理逻辑直观易懂,适合对ORM嵌套聚合不太熟悉的场景。

内容的提问来源于stack exchange,提问作者Dante

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 11:28:15