今天在使用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
总结:碰到问题时,不能简单地吞掉错误,而是需要从根本原因入手解决。