[英]Django ORM not generating correct SQL for many to many not in
我在使用 Django 从 ORM 生成的 SQL 时遇到问题。
Cartons
通过cartons_shipments
与Shipments
多对多关系。
我希望排除至少有一个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
。 如果你有一个显式的中间模型,这很容易,如果你没有,你首先需要一个Shipment
或Cartons
对象,因为据我所知你不能从类本身获得对中间模型的引用,只能从一个实例.
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.