DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

SQLAlchemy merge 只传部分子项,旧记录外键为何变成 NULL

区分集合整体替换与局部追加,检查真实SQL和关系配置,避免旧子项解除关联或被删除。

更新一个有两条价格记录的商品,只给 Session.merge() 传入商品 ID 和一条新价格。flush 后旧价格没有保留关联,生成的 UPDATE 反而把它们的商品外键设为 NULL。先检查传入的一对多集合是否被当成这次合并的完整成员列表。

这里针对可写的一对多 relationship,以及已经赋值、已加载的传入集合。SQLAlchemy 2.0 的 merge 会把来源对象的状态复制、协调到目标对象;它不会自动把“请求只带一条子项”解释成“追加这一条,其余全部不动”。

没传属性,与传入一个不完整集合不同

假设现有映射中 Item.item_prices 是可写的一对多关系。下面仅展示传入对象的结构,Item、ItemPrice 和编号应对应实际模型与已核对的业务对象,不是可直接用于生产的更新程序:

incoming = Item(id=item_id, item_prices=[new_price])

这个集合明确提供了一个成员。merge 默认的加载过程会与数据库中的集合协调,原有成员如果不在传入列表中,可能被解除关系。在可空外键且没有删除孤儿语义的配置下,解除关系就可能表现为旧记录外键变为 NULL;具体 SQL 还取决于映射、约束和级联配置。

没有提供集合属性则是另一种输入状态,不能与空列表或只有部分成员的列表混为一谈。构造请求对象、模型默认值和序列化过程,都可能无意中把未传字段变成已提供的空集合。

检查真实 flush SQL、传入集合与原有成员的主键,并核对哪些字段是缺省、哪些是明确赋值。避免在日志中输出敏感业务内容。merge 返回的是 Session 内的目标对象,来源对象本身不会因此自动变成这个目标。

追加或局部修改,直接操作已加载的业务对象

如果接口语义是追加价格,先在拥有这次事务的 Session 中读取目标商品与需要保留的集合,检查访问权限,再向该集合追加新的价格对象。更新已有价格时,按子项主键定位,并确认它确实属于这个商品;不能只接收一个子项 ID 就跨商品修改。

这种流程把“保留现有成员”写进实际操作,不要求 merge 猜测请求意图。集合读取、并发策略和写入应位于正确的事务范围;异步 ORM 还应预先或显式等待加载,避免普通属性访问触发隐式查询。

如果接口确实要求整体替换,就应传入完整、经过授权验证的成员集合,并明确遗漏成员代表解除关联还是删除。子项并发变更、重复主键、外键不可空及历史记录保留规则,都需要在接受请求前确定。

把 merge(load=False) 当成任意 PATCH 的快捷方式也不合适。它对传入状态有前提,并不会替应用定义缺失成员如何处理。类似的局部更新误解也会出现在 JSON 字段中,可对照JSONB 更新后未传字段丢失的原因,但 JSON 值替换与 ORM 关系协调是不同机制。

改反向关系或级联,不能补上请求语义

仅在子项指向父项的反向属性上设置 viewonly=True,不会冻结父项上仍然可写的集合。需要逐个检查实际参与写入的 relationship,不能看到某一端只读就认为整条关系不会变化。

给关系加 delete-orphan 更不能作为外键变 NULL 的通用修复。这会改变解除关联时的含义,可能把旧子项删除,而业务原本只是想保留它们。

SQLAlchemy 1.4 的旧式 Session 还需要留意双向关系赋值带来的反向 save-update 级联。1.4 的 future=True Session 与 SQLAlchemy 2.0 的行为不同于旧式路径;2.0 已移除旧的 cascade_backrefs 行为。这个版本差异需要核对,但 future=True 本身不会让不完整集合自动获得追加语义。

修改前备份受影响的父子记录和映射代码。在隔离副本覆盖未传集合、空集合、追加一项、修改已有项、整体替换及并发更新,核对 flush SQL 和最终关联。若旧关联已经提交为 NULL,改好代码不会自动找回原父项,应依据保存的业务记录或备份恢复;不要用猜测的父项 ID 批量补外键。