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

如何在Django中嵌套使用ArrayAgg?工时单关联查询报错解决

问题解决:Django嵌套ArrayAgg查询工时单关联设备及附件报错

需求

查询指定项目和日期的工时单,同时获取每个工时单关联的所有设备,以及每个设备对应的所有附件。原查询工时单及关联设备的代码可正常运行,但尝试在ArrayAgg的JSONObject中嵌套另一个ArrayAgg以获取设备附件时,触发FieldError。

模型定义

class TimeSheet(CommonModel):
    employee = models.ForeignKey(
        "employee.Employee", related_name="timesheets", on_delete=models.PROTECT
    )
    project = models.ForeignKey(
        "core.Project", related_name="employee_timesheets", on_delete=models.PROTECT
    )
    timesheet_date = models.DateField()
    clock_in_time = models.DateTimeField()
    clock_out_time = models.DateTimeField(null=True)


class TimesheetEquipment(CommonModel):
    equipment = models.ForeignKey(
        "equipment.Equipment",
        on_delete=models.PROTECT,
        related_name="timesheet_equipments",
    )
    resource_code = models.CharField(max_length=150)
    resource_name = models.CharField(max_length=511)
    rate = models.DecimalField(
        max_digits=30, decimal_places=decimal.ACCEPTED_DECIMAL_PLACE
    )
    unit = models.CharField(max_length=63, default="Hour")
    timesheet = models.ForeignKey(
        "timesheet.Timesheet",
        on_delete=models.CASCADE,
        related_name="timesheet_equipments",
    )
    equipment_attachments = models.ManyToManyField(
        "costcode.ResourceCode",
        related_name="timesheet_equipments",
        blank=True,
        through="timesheet.TimesheetEquipmentAttachment",
        help_text="Other Resources to be attached with this requirement",
    )
 

class TimesheetEquipmentAttachment(models.Model):
    timesheetequipment = models.ForeignKey(
        TimesheetEquipment,
        on_delete=models.CASCADE,
        related_name="timesheetequipmentattachments",
    )
    resource = models.ForeignKey(
        "resource_management.CostSheetResource",
        related_name="timesheetequipmentattachments",
        on_delete=models.PROTECT,
        null=True,
    )
    resource_code = models.CharField(max_length=150)
    resource_name = models.CharField(max_length=150)

正常运行的查询代码

from django.db.models.functions import JSONObject
from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import F

timesheets = (
            TimeSheet.objects.filter(project_id=project_id, timesheet_date=date)
            .select_related("employee")
            .prefetch_related(
                "timesheet_equipments",
                "timesheet_equipments__timesheetequipmentattachments",
            )
            .values("id")
            .annotate(
                timesheet_date=F("timesheet_date"),
                clock_in_time=F("clock_in_time"),
                clock_out_time=F("clock_out_time"),
                timesheet_equipments=ArrayAgg(
                    JSONObject(
                        equipment=F("timesheet_equipments__equipment_id"),
                        equipment_name=F(
                            "timesheet_equipments__equipment__equipment_name"
                        ),
                        resource_code=F("timesheet_equipments__resource_code"),
                        resource_name=F("timesheet_equipments__resource_name"),
                        rate=F("timesheet_equipments__rate"),
                        unit=F("timesheet_equipments__unit"),
                      ),
                    distinct=True,
                ),
            )
            .values(
                "id",
                "timesheet_date",
                "clock_in_time",
                "clock_out_time",
                "timesheet_equipments",
            )
            .order_by("-timesheet_date")
            .distinct()
        )

报错的嵌套查询代码

from django.db.models.functions import JSONObject
from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import F

timesheets = (
            TimeSheet.objects.filter(project_id=project_id, timesheet_date=date)
            .select_related("employee")
            .prefetch_related(
                "timesheet_equipments",
                "timesheet_equipments__timesheetequipmentattachments",
            )
            .values("id")
            .annotate(
                timesheet_date=F("timesheet_date"),
                clock_in_time=F("clock_in_time"),
                clock_out_time=F("clock_out_time"),
                timesheet_equipments=ArrayAgg(
                    JSONObject(
                        equipment=F("timesheet_equipments__equipment_id"),
                        equipment_name=F(
                            "timesheet_equipments__equipment__equipment_name"
                        ),
                        resource_code=F("timesheet_equipments__resource_code"),
                        resource_name=F("timesheet_equipments__resource_name"),
                        rate=F("timesheet_equipments__rate"),
                        unit=F("timesheet_equipments__unit"),
                      equipment_attachments=ArrayAgg(
                            JSONObject(
                                resource_code=F(
                                    "timesheet_equipments__timesheetequipmentattachments__resource_code"
                                ),
                                resource_name=F(
                                    "timesheet_equipments__timesheetequipmentattachments__resource_name"
                                ),
                                rate=F(
                                    "timesheet_equipments__timesheetequipmentattachments__rate"
                                ),
                            ),
                            distinct=True,
                        ),
                       
                    ),
                    distinct=True,
                ),
            )
            .values(
                "id",
                "timesheet_date",
                "clock_in_time",
                "clock_out_time",
                "timesheet_equipments",
            )
            .order_by("-timesheet_date")
            .distinct()
        )

错误信息(翻译后)

FieldError: 无法计算None('JSONObject(...)'): 'JSONObject(...)' 是一个聚合函数

错误原因

Django ORM不允许在一个聚合函数(如ArrayAgg)内部的JSONObject中嵌套另一个聚合函数,这是因为SQL层面不支持这种嵌套聚合的语法,Django的查询解析器也无法处理这种嵌套结构。


解决方案

方案一:Python层面组装嵌套数据(推荐,简单直观)

利用prefetch_related预取所有关联数据,然后在Python中手动构造需要的嵌套结构,避免数据库层面的复杂聚合嵌套。

from django.db.models import F

# 查询基础数据并预取关联
timesheets_queryset = (
    TimeSheet.objects.filter(project_id=project_id, timesheet_date=date)
    .select_related("employee")
    .prefetch_related(
        "timesheet_equipments",
        "timesheet_equipments__timesheetequipmentattachments",
    )
    .order_by("-timesheet_date")
)

# 手动构造嵌套结构
result = []
for timesheet in timesheets_queryset:
    equipment_list = []
    for equipment in timesheet.timesheet_equipments.all():
        # 收集当前设备的附件
        attachments = [
            {
                "resource_code": att.resource_code,
                "resource_name": att.resource_name,
                "rate": att.resource.rate if att.resource else None  # 根据实际模型调整
            }
            for att in equipment.timesheetequipmentattachments.all()
        ]
        equipment_data = {
            "equipment": equipment.equipment_id,
            "equipment_name": equipment.equipment.equipment_name,
            "resource_code": equipment.resource_code,
            "resource_name": equipment.resource_name,
            "rate": equipment.rate,
            "unit": equipment.unit,
            "equipment_attachments": attachments
        }
        equipment_list.append(equipment_data)
    
    timesheet_data = {
        "id": timesheet.id,
        "timesheet_date": timesheet.timesheet_date,
        "clock_in_time": timesheet.clock_in_time,
        "clock_out_time": timesheet.clock_out_time,
        "timesheet_equipments": equipment_list
    }
    result.append(timesheet_data)

方案二:数据库层面分步聚合(适合大数据量场景)

先给TimesheetEquipment模型注解附件的聚合结果,再在TimeSheet的查询中引用这个注解,避免直接嵌套聚合。

from django.db.models import Subquery, OuterRef
from django.db.models.functions import JSONObject
from django.contrib.postgres.aggregates import ArrayAgg

# 第一步:给每个TimesheetEquipment注解附件的聚合
equipment_with_attachments = TimesheetEquipment.objects.filter(
    id=OuterRef("timesheet_equipments__id")
).annotate(
    attachments=ArrayAgg(
        JSONObject(
            resource_code=F("timesheetequipmentattachments__resource_code"),
            resource_name=F("timesheetequipmentattachments__resource_name"),
            rate=F("timesheetequipmentattachments__resource__rate")  # 根据实际模型调整
        ),
        distinct=True
    )
).values("attachments")

# 第二步:查询TimeSheet并关联设备及附件
timesheets = (
    TimeSheet.objects.filter(project_id=project_id, timesheet_date=date)
    .values("id")
    .annotate(
        timesheet_date=F("timesheet_date"),
        clock_in_time=F("clock_in_time"),
        clock_out_time=F("clock_out_time"),
        timesheet_equipments=ArrayAgg(
            JSONObject(
                equipment=F("timesheet_equipments__equipment_id"),
                equipment_name=F("timesheet_equipments__equipment__equipment_name"),
                resource_code=F("timesheet_equipments__resource_code"),
                resource_name=F("timesheet_equipments__resource_name"),
                rate=F("timesheet_equipments__rate"),
                unit=F("timesheet_equipments__unit"),
                equipment_attachments=Subquery(equipment_with_attachments[:1])
            ),
            distinct=True
        )
    )
    .values(
        "id", "timesheet_date", "clock_in_time", "clock_out_time", "timesheet_equipments"
    )
    .order_by("-timesheet_date")
    .distinct()
)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 23:49:55