Python 记录一个 SqlAlchemy ORM 无法直接保存数据的问题以及解决方法

骷髅弓箭手 · 2022年01月27日 · 最后由 陈恒捷 回复于 2022年01月28日 · 3275 次阅读

这个问题困扰了我很久,为什么一直没解决,主要是因为有替换方案可以选择,所以一直搁置了。

现在随着业务发展,数据结构、存储算法越来越复杂,绕路的方案在某些时候就变得不可接受了,所以需要去解决这个问题。

什么问题

正常的 ORM 模式,如果需要更新某条数据的话,有两个办法。

user = session.query(User).filter(User.id == 1).first()
user.name = 'KL'
session.commit()

或者

session.query(User).filter(User.id == 1).update({
    'name': 'KL'
})

我目前碰到的问题是,第一种方式不可用,第二种方式可用。

熟悉 ORM 的朋友应该都知道,第二种方式是即时生效的,默认带上了 commit 方法。

这就导致每次更新都会去请求数据库进行插入操作,非常耗时。在业务简单的时候,影响不大,但是随着业务发展,这种更新方式在有些时候就变得不可接受了。

最初我非常懵逼,不明白为什么会不可用,然后我重新看了官方文档里数据更新相关的章节。

找到了两个可以查看 session 缓存的方法。

发现问题关键点

session.dirty
session.new

session.dirty 表示和当前数据表不一致的数据,也就是你修改过的数据。

session.new 表示不再当前数据表的数据,也就是你新增的数据。

当我使用第一种方法去更新数据时,发现 session.dirty 中没有存入任何数据,这让我很惊讶,经验告诉我,这是在赋值的时候就出现了问题。

于是我找到了我的映射类。

class Account(zda_engine.Base):

    __tablename__ = 'account'

    id = Column(Integer, primary_key=True, autoincrement=True, nullable=False, unique=True)
    email = Column(String, nullable=False)
    password = Column(String, nullable=False)

    def __setattr__(self, key, value):
        if key == 'password':
            self.__dict__[key] = hashlib.md5(value.encode(encoding='UTF-8')).hexdigest()
        else:
            self.__dict__[key] = value

为了方便展示,我把__setattr__方法移到了子类中,实际使用时,更复杂一些。

可以看到,我在自己定义的类里面完全重写了__setattr__方法,一直以来这个方法可以正常的更新实例值,所以我一直觉得没有问题。

不过在session.dirty方法中,我判断了一下是设置值时出现的问题,那么再看这段代码时,就感觉处处是问题了。

最关键的就是,它覆盖了 SqlAlchemy 的__setattr__方法,最终导致后续一系列的数据存储动作都出现了问题,因为代码没跑过去。

解决问题

当然,在正式修改成功之前,这都是我的推测,于是我将__setattr__代码修改为下面这样

def __setattr__(self, key, value):
    if key == 'password':
        value = hashlib.md5(value.encode(encoding='UTF-8')).hexdigest()
    super().__setattr__(key, value)

这样的话,既能将特定的字段修改为我想要的值,同时依旧可以调用基类中的__setattr__方法。

经过调试,最终解决这个问题。

总结

基础方法使用时,会带来非常多非常好的体验,同时也可能会和其他广泛使用的三方库发生冲突,在使用这些方法时,最好要重新调用一下基类方法,然后再去实现自己的逻辑。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 7 条回复 时间 点赞

我一般都是处理好数据,再循环 setattr

......这个是你自己挖的坑呐......

这个主要还是习惯问题。override 别人的方法前,要先了解清楚这个方法是干嘛的,override 是不是最佳实现方式,特别是 override 后原来已有的功能是否有重新提供,没有的话是否要通过调用原来父类方法来提供原有的能力。

看楼主这个 setattr 方法的最终实现,实际就是插入了个如果是密码就哈希后再写入的动作。这个动作为何要放在 orm 对象的 set 内部这么深的位置,而非在业务逻辑代码里写 account.password = hashlib.md5(password.encode(encoding='UTF-8')).hexdigest()

class LoginBody(BaseModel):
    account: str
    password: str

    @validator('account','password')
    def validatorEmpty(cls, value):
        return ToolsSchemas.not_empty(value)

    @validator('password')
    def md5_paw(cls, value):
        m = hashlib.md5()
        m.update(value.encode("utf-8"))
        return m.hexdigest()

fastapi 的

陈恒捷 回复

确实是习惯问题,这次属于暴雷了,哈哈。
帖子中的举例只是场景中的某一种,之所以要这么做,是因为想把对数据的定向修改放到统一的地方,这样在写业务逻辑时可以分层考虑问题,不会将逻辑处理和数据处理的代码混在一起。

这里只是举了一个简单场景,实际业务会远比登录复杂,所以在设计时进行了业务逻辑和数据处理的分层。

这个看团队的代码规范要求吧。我们一般要求保持 set 方法的纯粹(毕竟没多少人会想到你在对象的 set 方法里做固定的数据转换操作),这类数据转换逻辑可以通过私有方法封装来避免和业务逻辑揉在一块,但不会直接改写 set 方法做转换。这种写法藏得太深了。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册