From b79b0f99af84cf47c5ceaeb325aab5fdfc94d66c Mon Sep 17 00:00:00 2001 From: DXC Date: Tue, 16 Jun 2026 13:55:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=80=9F=E5=BA=93=E6=89=AB=E7=A0=81?= =?UTF-8?q?=E5=87=BA=E5=BA=93):=20=E6=92=A4=E9=94=80=20joinedload=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20PG=20"FOR=20UPDATE=20cannot=20be=20applied?= =?UTF-8?q?=20to=20nullable=20side=20of=20outer=20join"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 83b3db6 引入的 joinedload(ModelClass.base) 触发 LEFT OUTER JOIN, 而 with_for_update() 会被 SQLAlchemy 透传到 join 的 nullable 侧, PG 直接抛 FeatureNotSupported,且连表加锁有死锁风险 - 退回最安全的单表 FOR UPDATE 模式,接受 N+1 lazy 加载的代价 - 在 防线3 上方加防回归注释,明确禁止未来再加 joinedload - process_return 中的另两处 joinedload 不带 FOR UPDATE,不受 PG 限制,保留 --- inventory-backend/app/services/trans_service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/inventory-backend/app/services/trans_service.py b/inventory-backend/app/services/trans_service.py index 6fddfc3..df20fa9 100644 --- a/inventory-backend/app/services/trans_service.py +++ b/inventory-backend/app/services/trans_service.py @@ -95,8 +95,11 @@ class TransService: # ============================================== # ★ 防线3:并发超卖与负库存 - 锁行后再查可用库存 + # ⚠️ 不要在此加 joinedload(ModelClass.base)!PG 禁止 FOR UPDATE + # 应用到 outer join 的 nullable 侧,会报 FeatureNotSupported + # 并有死锁风险。stock.base 走单条 lazy 加载是已知取舍。 # ============================================== - stock = ModelClass.query.options(joinedload(ModelClass.base)).with_for_update().get(stock_id) + stock = ModelClass.query.with_for_update().get(stock_id) if not stock: raise ValueError(f"库存不存在 ID:{stock_id}") # ==============================================