1. 问题
摘要 :SQLAlchemy 的
values()与ordered_values()在普通更新时表现一致,但当某一列需要引用另一列的当前值时,两者生成的 SQL 在SET子句字段顺序上存在隐藏差异。values()内部会按列名排序,无法控制顺序;而ordered_values()严格保留传入顺序。若列间存在相互引用,必须使用ordered_values()才能保证求值顺序正确,否则可能得到错误结果。
在 SQLAlchemy 中,Update 对象提供了 values() 与 ordered_values() 两种方式来指定要更新的字段。大多数时候它们看起来没什么差别,都是一样的更新,似乎只有数据提供方式的不同。但当你需要让某一列引用另一列的当前值时,两者会暴露出一个容易被忽略的隐藏差异。
先看一张简单的记录表结构:
class RecordPO(BasePO):
"""
记录表
"""
__tablename__ = "sys_record"
id = Column("id", Integer, comment="主键", autoincrement=True, primary_key=True)
name = Column("name", String(64), comment="名称")
state = Column("state", String(64), comment="状态")
previousStatus = Column("previous_status", String(64))
ipAddress = Column("ip_address", String(64))
webBrowser=Column[str]("web_browser",String(255))
message = Column("message", String(255))
update_by_values = Update(RecordPO).where(RecordPO.id == 1).values({RecordPO.name:"test_1",RecordPO.state:"failed"})
update_by_ordered= Update(RecordPO).where(RecordPO.id == 1).ordered_values((RecordPO.name,"test_1"),(RecordPO.state,"failed"))
但是如果需要这样更新:
update_by_values = Update(RecordPO).where(RecordPO.id == 1).values({RecordPO.previousStatus:RecordPO.state,RecordPO.state:"failed"})
>UPDATE sys_record SET state=%s, previous_status=sys_record.state WHERE sys_record.id = %s
update_by_ordered= Update(RecordPO).where(RecordPO.id == 1).ordered_values((RecordPO.previousStatus,RecordPO.state),(RecordPO.state,"failed"))
>UPDATE sys_record SET previous_status=sys_record.state, state=%s WHERE sys_record.id = %s
不知道各位是否看出了问题?
2. 问题剖析:为什么 SQL 顺序会不同?
对比上面两条 SQL,核心差异在于 SET 子句中字段的排列顺序:
- 使用
values()时,生成的 SQL 是SET state=%s, previous_status=sys_record.state,即state在前、previous_status在后; - 使用
ordered_values()时,生成的 SQL 是SET previous_status=sys_record.state, state=%s,即previous_status在前、state在后。
这个顺序差异在绝大多数数据库上看似无关紧要,因为 SQL 标准并不保证 SET 子句的求值顺序。但问题恰恰出在这里:当某一列的赋值引用了另一列的当前值时,数据库实际执行时可能按 SET 子句的书写顺序逐列求值。
以 values() 生成的 SQL 为例:
sql
UPDATE sys_record SET state=%s, previous_status=sys_record.state WHERE sys_record.id = %s
如果数据库按从左到右的顺序执行,那么 state 先被更新为 "failed",随后 previous_status 再取 sys_record.state 时,拿到的已经是更新后的 "failed",而不是更新前的原始状态。这会导致 previous_status 被错误地写成 "failed",丢失了原始状态。
而 ordered_values() 生成的 SQL 把 previous_status 放在前面:
sql
UPDATE sys_record SET previous_status=sys_record.state, state=%s WHERE sys_record.id = %s
此时 previous_status 先取 sys_record.state 的当前值(即更新前的原始状态),随后 state 才被更新为 "failed",从而正确保留了原始状态。
3. 为什么 ordered_values() 能保证顺序?
ordered_values() 的设计初衷,就是显式保留调用者传入的字段顺序 。它接收一个二元组列表,每个二元组是 (列, 值),SQLAlchemy 会严格按照这个列表的顺序生成 SET 子句。
而 values() 接收的是一个字典。在 Python 3.7+ 中,字典虽然保持插入顺序,但 SQLAlchemy 在内部处理时,会先对字典的键做一次排序(按列名或属性名的字母序),再生成 SQL。因此 values() 生成的 SET 子句顺序,并不一定等于你书写字典时的顺序,而是经过排序后的结果。
这正是两者最本质的区别:
| 方法 | 参数形式 | SET 子句顺序 | 适用场景 |
|---|---|---|---|
values() |
字典 | 按列名排序,不保证书写顺序 | 普通字段更新,无列间引用 |
ordered_values() |
二元组列表 | 严格按传入顺序 | 列间存在相互引用时 |
4. 实际影响:什么时候必须用 ordered_values()?
当更新语句中某一列的值引用了另一列的当前值 时,就必须使用 ordered_values() 来保证求值顺序正确。典型场景包括:
- 状态流转:把
previous_status设为当前state,再把state更新为新值; - 计数器累加:
counter = counter + 1这类自增操作; - 字段互换:交换两列的值,如
a = b, b = a。
反之,如果只是普通的多字段更新,各列之间互不引用,那么 values() 和 ordered_values() 的结果完全一致,用哪个都行。
5. 总结
values()适合普通更新,内部会对字段排序,无法控制SET子句顺序;ordered_values()严格保留传入顺序,适合列间存在引用关系的更新;- 当某一列赋值依赖另一列当前值时,务必使用
ordered_values(),否则可能得到错误结果。
希望这篇文章能帮你避开这个隐藏的坑。如果你也遇到过类似问题,欢迎在评论区交流。