SQLAlchemy无法提交未修改字段值,如何触发客户端加密?
问题背景
我通过SymmetricEncryptionClientSide实现客户端加密,基于SQLAlchemy模型做数据入库自动加密、出库自动解密,新数据处理完全正常。现在要批量处理未加密的遗留数据,原本想查询数据后把原始值重新赋值给字段再提交,以此触发加密逻辑,但发现只有字段值改变时才会生效,赋值相同值完全没反应:
user = session.query(User).first() user.name = user.name session.commit() # 不会触发更新,数据也不会被加密
改成下面的写法就能正常提交并完成加密:
user.name = user.name + 'o' session.commit()
想搞清楚这背后的原因,以及怎么实现重赋相同值时也能触发提交?
原因分析
SQLAlchemy的Session默认只跟踪对象属性的变化:只有当新赋值的内容和当前内存中存储的旧值在Python层面不相等时,才会把这个属性标记为「脏数据」,进而在commit()时生成对应的UPDATE语句。
你的客户端加密逻辑是在模型字段的赋值/取值环节做的:出库时自动把数据库里的密文解密成明文返回给你,入库时自动把明文加密成密文存储。但当你执行user.name = user.name时,SQLAlchemy拿到的是解密后的明文,新值和旧值(内存里的明文)完全一致,所以不会判定这个属性有变化,自然不会生成UPDATE操作,加密逻辑也就不会被触发。
而改成user.name = user.name + 'o'时,新值和旧值不一样,SQLAlchemy标记属性为脏,提交时就会触发字段的加密逻辑,把新的明文加密后存入数据库。
解决方案
方法1:手动标记属性为脏数据
直接告诉SQLAlchemy这个字段需要更新,强制触发加密逻辑:
user = session.query(User).first() # 方式一:标记整个对象为脏 session.dirty.add(user) # 方式二:更精确地标记单个字段(推荐) from sqlalchemy.orm import attributes attributes.set_committed_value(user, 'name', user.name) session.commit()
方法2:修改模型字段的比较逻辑
如果需要长期复用这个逻辑,可以在加密字段上自定义比较规则,让SQLAlchemy即使看到明文相同,也认为需要触发更新。比如通过hybrid_property或者自定义字段类型的比较器来实现,不过需要修改模型定义。
方法3:执行原生SQL强制更新
如果是一次性处理遗留数据,直接用原生SQL绕开ORM的变化跟踪,强制更新字段(前提是你的加密逻辑能在字段的更新钩子中触发):
session.execute( "UPDATE user SET name = name WHERE id = :user_id", {"user_id": user.id} ) session.commit()
这种方式直接触发数据库层面的更新,会让模型的加密逻辑(比如before_update钩子)生效。
方法4:临时修改值再回滚(临时方案)
和你测试的思路类似,适合小批量数据处理:
user = session.query(User).first() # 先设置临时值触发脏标记 temp_val = user.name + "_tmp" user.name = temp_val # 再改回原数值 user.name = user.name.replace("_tmp", "") session.commit()
这种方式属于临时hack,不推荐用于大量数据。
内容的提问来源于stack exchange,提问作者Kakedis

