今天在使用sqlalchemy时,出现一例sqlalchemy.orm.exc.DetachedInstanceError异常,值得简单记录一下过程
问题出现
Traceback (most recent call last): File "/Users/dennis/Documents/Projects/PythonProjects/TenMock/tencloud-sdk/tensdk/store/key_pair/service.py", line 166, in <module> service.create_account('123Sadfaf', 'test', '', 'data') File "/Users/dennis/Documents/Projects/PythonProjects/TenMock/tencloud-sdk/tensdk/core/decorator.py", line 28, in wrapper return func(*args, **kwargs) File "/Users/dennis/Documents/Projects/PythonProjects/TenMock/tencloud-sdk/tensdk/store/key_pair/service.py", line 58, in create_account return biz.id File "/Users/dennis/PycharmProjects/tencloud-sdk/lib/python3.7/site-packages/sqlalchemy/orm/attributes.py", line 465, in __get__ return self.impl.get(state, dict_) File "/Users/dennis/PycharmProjects/tencloud-sdk/lib/python3.7/site-packages/sqlalchemy/orm/attributes.py", line 906, in get value = state._load_expired(state, passive) File "/Users/dennis/PycharmProjects/tencloud-sdk/lib/python3.7/site-packages/sqlalchemy/orm/state.py", line 667, in _load_expired self.manager.expired_attribute_loader(self, toload, passive) File "/Users/dennis/PycharmProjects/tencloud-sdk/lib/python3.7/site-packages/sqlalchemy/orm/loading.py", line 1345, in load_scalar_attributes "attribute refresh operation cannot proceed" % (state_str(state)) sqlalchemy.orm.exc.DetachedInstanceError: Instance <TenAccountInfo at 0x1081e0d50> is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: http://sqlalche.me/e/14/bhk3)
异常很明显指向service.py中的create_account函数,我们来看一下该函数。
@validated(CreateAccountOfKeyPairSchema) def create_account(self, app: str, subject: str, custom: str, data: str) -> str: biz = TenAccountInfo() biz.id = self._account_info_id_generator.next() biz.app = app biz.subject = subject biz.custom = custom biz.data = data biz.invalid = 0 biz.version = self._version_generator.next() self._account_info_dao.save(biz) return biz.id
粗粗一看,觉得挺正常的。做为习惯面向百度编程的我,把sqlalchemy.orm.exc.DetachedInstanceError
在百度上一搜,还真有解决方案。网上方案基本是修改sessionmaker参数(expire_on_commit=False)。
Session = sessionmaker(bind=engine, expire_on_commit=False)
于是,尝试着改了一下,果然问题解决。
问题到这里结束了么?当然没有。
expire_on_commit这个参数缺省值为True,看名字就能知道个七七八八,我也不多说(怕说错误导大家)。改了缺省值,总是不得劲,直觉会影响效率。
我回过头又看了看代码错误的地方create_account,最特殊的就在最后两行:
self._account_info_dao.save(biz) return biz.id
本意是保存了之后,再返回业务对象biz的id。
在看看save的代码:
def save(self, biz: TenAccountInfo) -> bool: with self._session_cls() as session: try: session.add(biz) session.commit() except IntegrityError as ex: print(ex.__cause__) session.rollback() return False else: return True
这里使用with语句,会自动执行session.close()。嗯,保持及时释放资源是个好习惯,不应该有错误。
所以,综合上述两段代码,可以猜想,错误是在session.close()后,return biz.id时,系统重新尝试使用session从数据库中刷新biz对象,而得到的错误。
于是,针对当前问题,可以通过保存id的方式,简单地解决。
解决此次问题的代码如下
@validated(CreateAccountOfKeyPairSchema) def create_account(self, app: str, subject: str, custom: str, data: str) -> str: biz = TenAccountInfo() biz.id = self._account_info_id_generator.next() biz.app = app biz.subject = subject biz.custom = custom biz.data = data biz.invalid = 0 biz.version = self._version_generator.next() ret = biz.id self._account_info_dao.save(biz) return ret
总结:碰到问题时,不能简单地吞掉错误,而是需要从根本原因入手解决。