DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

SQLAlchemy 修改 JSON 字典后 commit 没有保存

普通 JSON 映射不自动检测字典原地修改。为顶层变化配置 MutableDict,或修改独立副本后整体赋回,并用新会话检查实际落库结果。

SQLAlchemy 对象里的 JSON 字典已经变了,调用 session.commit() 也没有报错,但重新查询仍得到旧值。常见触发方式是直接修改现有字典:

item.settings.update({"theme": "dark"})
session.commit()

如果该字段只是普通 JSON 映射,这次原地修改可能没有进入 ORM 的变更记录。提交事务不会替 ORM 自动发现任意 Python 对象内部发生的变化。

下面使用 SQLAlchemy 2.0 的映射写法,处理的是 JSON 对象顶层键的修改。字段已配置自定义可变类型、对象已脱离会话,或者数据库确实收到 UPDATE 却出现其他结果时,需要按相应情况排查。

先区分没有发出更新和更新后读错值

在开发环境观察本次提交的 SQL,确认有没有针对该行和 JSON 列的 UPDATE。注意日志可能包含业务数据,不要把带凭据的 JSON 或连接串复制到工单中。

如果没有更新,再看操作是否只是 dict.update()、settings["theme"] = ... 之类的原地修改,以及映射是否只有 JSON。如果已经发出更新,则应检查提交是否成功、后续是否被覆盖,以及查询是否读取了正确的数据库和行;继续补可变类型不能解决这些问题。

不要仅凭同一个 Python 对象打印出了新值判定写入成功。那只能说明内存内容变了。

顶层字典修改交给 MutableDict 跟踪

将 JSON 字段映射为 MutableDict.as_mutable(JSON),可让顶层键的增删改通知 ORM。下面是只使用内存 SQLite 数据库的完整验证示例,需要 SQLAlchemy 2.0 以及支持 JSON 的 SQLite 运行库:

from sqlalchemy import JSON, create_engine
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column

class Base(DeclarativeBase):
    pass

class Setting(Base):
    __tablename__ = "settings_demo"
    id: Mapped[int] = mapped_column(primary_key=True)
    data: Mapped[dict] = mapped_column(MutableDict.as_mutable(JSON))

engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add(Setting(id=1, data={"theme": "light", "page_size": 20}))
    session.commit()

with Session(engine) as session:
    item = session.get(Setting, 1)
    item.data.update({"theme": "dark"})
    session.commit()

with Session(engine) as session:
    saved = session.get(Setting, 1)
    assert saved.data == {"theme": "dark", "page_size": 20}

最后一段用新的会话重新读取,核对修改的键和未修改的键。这里的断言是验证条件,不代表已经在你的环境执行成功。示例中的建表只用于内存数据库,不要把生产连接串代入后直接运行。

现有 PostgreSQL 字段如果使用 JSONB,保留该类型,只在 ORM 映射中用 MutableDict.as_mutable(JSONB) 包装。无需为了变更跟踪把数据库列改成另一种类型。修改映射后重新启动使用它的应用进程,再用新会话加载对象验证;不要认为热改类定义会自动改造所有已经载入的普通字典。

不改映射时,赋一个新对象

普通 JSON 映射也能检测整个属性被替换。对于顶层补丁,可以先复制再修改,最后把完整的新字典赋回属性:

updated = item.settings.copy()
updated.update({"theme": "dark"})
item.settings = updated
session.commit()

这段代码要求原值是字典。若字段允许 NULL、数组或其他 JSON 值,应先按业务规则处理类型,不能无条件调用 .copy() 和 .update()。

不要先修改原字典,再让 updated 指向同一个对象;那仍然没有保留一份独立的修改前结构。赋回时也要用合并后的 updated,而不是只含变更键的补丁,否则未列出的键会被整列替换丢掉。

MutableDict 不会递归跟踪嵌套对象

即使顶层已经使用 MutableDict,下面的嵌套修改也不能据此保证被检测:

item.settings["display"]["theme"] = "dark"

display 对应的普通字典内部变化没有自动向顶层传播。对于确实需要修改嵌套内容的普通 JSON 结构,可以在修改之前深复制,再整体赋回:

from copy import deepcopy

updated = deepcopy(item.settings)
updated["display"]["theme"] = "dark"
item.settings = updated
session.commit()

这里仍要求 display 已存在且是字典。深复制只解决这次修改的对象独立性,不会实现数据库端深度合并,也不能防止两个请求相互覆盖整列内容。并发更新需要另外设计冲突检测或数据库更新表达式。

上线前至少验证顶层修改、未改键保留,以及应用实际使用的嵌套操作。若映射调整不符合需求,回退应用代码并重新建立会话;这项修复本身不要求清空 JSON、删除记录或重建数据库。