繁体   English   中英

Django ORM 没有为多对多不生成正确的 SQL

[英]Django ORM not generating correct SQL for many to many not in

我在使用 Django 从 ORM 生成的 SQL 时遇到问题。

Cartons通过cartons_shipmentsShipments多对多关系。

我希望排除至少有一个INBOUND Carton 状态为['TRANSIT', 'DELIVERED', 'FAILURE']

但是我没有得到预期的结果,所以我打开了 SQL 日志记录。

return Shipment.objects.filter(
    ... # other filtering
    # does not have any inbound cartons in_transit/delivered/failed
    ~Q(
        Q(cartons__type='INBOUND') &
        Q(cartons__status__in=['TRANSIT', 'DELIVERED', 'FAILURE'])
    ) &
).distinct()

我也试过这个作为我的过滤器,但得到了相同的 SQL 输出。

~Q(
    cartons__type='INBOUND', 
    cartons__status__in=['TRANSIT', 'DELIVERED', 'FAILURE']
)

这将生成此 SQL:

AND NOT (
    "shipments"."id" IN (
        SELECT U1."shipment_id" 
        FROM "cartons_shipments" U1 
        INNER JOIN "cartons" U2 ON (U1."carton_id" = U2."id") 
        WHERE U2."type" = 'INBOUND'
    ) 
    AND "shipments"."id" IN (
        SELECT U1."shipment_id" FROM "cartons_shipments" U1 
        INNER JOIN "cartons" U2 ON (U1."carton_id" = U2."id") 
        WHERE U2."status" IN ('TRANSIT', 'DELIVERED', 'FAILURE')
    )
) 

但这不正确,因为它会排除有任何INBOUND纸箱的货件和任何纸箱(不一定是INBOUND纸箱)状态为['TRANSIT', 'DELIVERED', 'FAILURE']货件。 我需要结合这个逻辑。

此外,现在我正在运行 2 个子选择,并且性能受到显着影响,因为我们有大量处于这些状态的纸箱。

正确的 SQL 应该是这样的:

AND NOT ("shipments"."id" IN (
    SELECT U1."shipment_id" 
    FROM "cartons_shipments" U1 
    INNER JOIN "cartons" U2 ON (U1."carton_id" = U2."id") 
    WHERE U2."type" = 'INBOUND'
    and U2."status" IN ('TRANSIT', 'DELIVERED', 'FAILURE')
))

通过这种方式,我只会排除这些状态下带有INBOUND纸箱的货件。

这两者之间的查询时间很重要,当然我能够通过第二个 SQL 示例获得正确的结果。 我认为我可以通过组合Q()对象来组合该逻辑。 但是想不通。

我还认为也许我可以在第二个示例中正确使用原始 SQL。 但是我很难弄清楚如何将原始 sql 与其他 ORM 过滤器结合起来。

任何帮助将不胜感激。


编辑:

通过在代码中进行过滤并从查询中删除过滤器,我能够获得正确的结果:

returned_cartons = Carton.objects.prefetch_related('shipments').filter(
    type='INBOUND',
    status__in=['TRANSIT', 'DELIVERED', 'FAILURE']
)

returned_shipment_ids = list(map(
    lambda carton: carton.shipments.first().id,
    returned_cartons
))

return list(filter(
    lambda shipment: shipment.id not in returned_shipment_ids,
    shipments
))

不幸的是,这太慢而无用。


基于 Endre Both 想法的最终解决方案🙌

return Shipment.objects.filter(
    ...,  # other filtering
    # has at least 1 inbound carton
    Q(cartons__type='INBOUND')
).exclude(
    # we want to exclude shipments that have at least 1 inbound cartons
    # with a status in transit/delivered/failure
    id__in=Shipment.objects.filter(
        ...,  # filters to limit the number of records returned
        cartons__type='INBOUND',
        cartons__status__in=['TRANSIT', 'DELIVERED', 'FAILURE'],
    ).distinct()
).distinct()

这行Q(cartons__type='INBOUND')是必需的,因为我们排除了具有['TRANSIT', 'DELIVERED', 'FAILURE']状态的INBOUND Carton 的货件。 但我们也会保留没有任何纸箱的货件。

希望这能帮助更多的人。

对于我们凡人来说,ORM 中的“M”有时可能有点难以理解。 但是你可以尝试一种不同的、更简单的方法。 它仍然使用子查询而不是连接,但这不一定会拖累性能。

Shipment.objects.exclude(
    id__in=Cartons.objects
        .filter(type='INBOUND',
                status__in=['TRANSIT', 'DELIVERED', 'FAILURE'])
        .values('shipments__id')
        .distinct()
)

Carton模型返回到Shipment主键的引用的确切名称取决于模型的确切定义。 我已经使用过shipments__id ,但它可能是shipment_set__id或其他东西。


新想法:您需要将子选择基于中间模型而不是Cartons 如果你有一个显式的中间模型,这很容易,如果你没有,你首先需要一个ShipmentCartons对象,因为据我所知你不能从类本身获得对中间模型的引用,只能从一个实例.

IModel = Shipment.objects.first().cartons.through
Shipment.objects.exclude(
    id__in=IModel.objects
        .filter(cartons__type='INBOUND',
                cartons__status__in=['TRANSIT', 'DELIVERED', 'FAILURE'])
        .values('shipment__id')
        .distinct()
)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM