资讯专栏INFORMATION COLUMN

关于Flask-SQLAlchemy事务提交有趣的探讨

BlackHole1 / 1709人阅读

摘要:最近在开发的时候遇到这样一个问题我就好奇了这样还报不在中的错没有显示调用啊加一行测试无奈,一个一个翻到之间调用的每一个函数,终于在找到可疑点但是这里也没有显式提交。为什么接下来总结下大神们的探讨。

最近在开发mdwiki的时候遇到这样一个问题.Post is unbond to session.
我就好奇了

post=Post.query.filter_by(location=location).first()
abspath=util.getAbsPostPath(post.location)
tagsList=[]
...
print(post in session) #False
post.tags=tagsList

这样还报post不在session中的错?没有显示调用db.session.commit()啊.
加一行测试:
print(post in session) #False
无奈,一个一个翻post=Post.query.filter_by(location=location).first()到post.tags=tagsList之间调用的每一个函数,终于在util.getAbsPostPath找到可疑点

def getAbsPostPath(location):
    with current_app.app_context():              
        abspath=os.path.join(current_app.config["PAGE_DIR"],location.replace("/",os.sep))+".md"
    return abspath

但是这里也没有显式提交。只是多push了一个app_context,也不至于这样吧?
无奈之下查看Flask-SQLAlchemy源码,还好这货只有两个文件,比较少。
有这么一段:

        # 0.9 and later
        if hasattr(app, "teardown_appcontext"):
            teardown = app.teardown_appcontext
        # 0.7 to 0.8
        elif hasattr(app, "teardown_request"):
            teardown = app.teardown_request
        # Older Flask versions
        else:
            if app.config["SQLALCHEMY_COMMIT_ON_TEARDOWN"]:
                raise RuntimeError("Commit on teardown requires Flask >= 0.7")
            teardown = app.after_request

        @teardown
        def shutdown_session(response_or_exc):
            if app.config["SQLALCHEMY_COMMIT_ON_TEARDOWN"]:
                if response_or_exc is None:
                    self.session.commit()
            self.session.remove()
            return response_or_exc

这下明白了,原理是它监听了app.teardown_appcontext事件,在该事件发生时会调用
self.session.remove()移除session。这样一来把这一行注释掉,直接使用config模块就解决问题了

def getAbsPostPath(location):
       # with current_app.app_context(): 
       abspath=os.path.join(config.PAGE_DIR,location.replace("/",os.sep))+".md"  

但同时看到了这个选项SQLALCHEMY_COMMIT_ON_TEARDOWN,是不是Flask-SQLAlchemy可以配置请求执行完逻辑之后自动提交,而不用我们每次都手动调用session.commit()?通过源码看答案是肯定的。
但是好奇的我还是google之,然后在github上看到了这样几段有趣的讨论:先贴地址
https://github.com/mitsuhiko/...
https://github.com/mitsuhiko/...
https://github.com/rosariomgo...

然后在官网看到这样一段:

Consider SQLALCHEMY_COMMIT_ON_TEARDOWN harmful and remove from docs.

什么?考虑移除这一特性?
刚刚知道这么方便的特性,准备用来着,就要被移除?更何况源码中也没有提示要移除啊。
其实是这样的,作者准备在3.0版本移除SQLALCHEMY_COMMIT_ON_TEARDOWN这一特性,目前自2.1以后从文档中移除了相关介绍。

为什么?接下来总结下大神们的探讨。

mattupstate commented on 31 Jan 2015: I"d guess that the reason is due
to the teardown_appcontext callback carrying a bug that, even if you
catch an exception during the app context, the response_or_exc will
never be None. In other words, teardown_appcontext suffers from a
general Python exception handling bug.

这位mattupstate说teardown_appcontext回调存在一个bug,就是即使你正确地捕获了所有的bug,但是回调函数的第一个参数response_or_exc仍然不会为None。这一点令人费解。于是我试验了一发,包括没有bug的情形,主动抛出并捕获的情形,以及after_request中捕获并抛出的情形,都发现response_or_exc为None,没有重现他所说的。why?好想知道为什么。猜测可能是我Python版本,Flask版本的关系?继续看吧

immunda commented on 3 Feb 2015 Sorry for the silence on this. Yep,
that"s the motivation, moving away from the (flawed) magic. I"m
waiting to deprecate it entirely (3.0), because there"s plans to
introduce a more explicit transaction decorator first.

我去,连Flask-SQLAlchemy作者都支持这一观点,好吧,虽然我没有重现该问题,但是还是就这么认为吧,不用这个特性了。但是还是好奇地看了一下其他的观点。

原来实际上问题是这样的,见https://github.com/mitsuhiko/...

先贴上FLask-SQLAlchemy那部分代码:

       @teardown
        def shutdown_session(response_or_exc):
            if app.config["SQLALCHEMY_COMMIT_ON_TEARDOWN"]:
                if response_or_exc is None:
                    self.session.commit()
            self.session.remove()
            return response_or_exc

如果在app.teardown_request中或者在self.session.commit()时发生异常,而这个异常在这里并没有被捕获,那么self.session.remove()也就没有执行,那么这就会影响到下一个请求,下一个请求获取到的session其实是上一个带回滚状态的session,从而导致请求没有按预期效果执行而失败。至此问题算明白了。并不是mattupstate这哥们形容的那样。那么这应该是flask实现机制导致的吧。继续挖。
https://github.com/pallets/fl...
http://stackoverflow.com/ques...
这哥们garaden给Flask提交了代码合并请求,关键部分如下

+    def wrap_teardown_func(teardown_func):
 +        @wraps(teardown_func)
 +        def log_teardown_error(*args, **kwargs):
 +            try:
 +                teardown_func(*args, **kwargs)
 +            except Exception as exc:
 +                app.logger.exception(exc)
 +        return log_teardown_error
 +
 +    if app.teardown_request_funcs:
 +        for bp, func_list in app.teardown_request_funcs.items():
 +            for i, func in enumerate(func_list):
 +                app.teardown_request_funcs[bp][i] = wrap_teardown_func(func)
 +    if app.teardown_appcontext_funcs:
 +        for i, func in enumerate(app.teardown_appcontext_funcs):
 +            app.teardown_appcontext_funcs[i] = wrap_teardown_func(func)

如果合并了这部分代码之后,那么以后注册app.teardown_request和app.teardown_appcontext,时异常将会自动被捕获。这在https://github.com/pallets/fl...可以看到新版本Flask已经合并了这部分代码,不存在该问题了。
后面讨论看到

Recently PR pallets/flask#1822 got merged into Flask. Will this maybe
change the fact whether SQLALCHEMY_COMMIT_ON_TEARDOWN will still be
removed in future?

但这对于解决FLask-SQLAlchemy中的问题好像还是没有帮助?是不是我理解错了?如果session.commit发生异常,session.remove这样还是不会执行?
后面看了https://github.com/pallets/fl...中的代码,优化了application context从栈中pop的逻辑,这次的代码提交确保了tear_down回调处理发生异常时不会导致application context无法从栈中弹出而影响后续请求。这下大致明白了。Flask-SQLAlchemy中的db.session依赖于Application Context,所以如果这次Flask能确保无论如何最后会正确弹出application context,那么db.session也随之销毁了,那就不存在后续的影响了。但是,最后这句话我也不敢保证,只能是猜想。

所以,言归正传,如果不用SQLALCHEMY_COMMIT_ON_TEARDOWN这一特性,那么我们怎么确保每次自动提交session呢?
第一种:不是自动,全手动模式commit(),看讨论还是有很多人喜欢这种方式的,不过我讨厌每次都调用commit()
第二种:在after_request中进行提交commit,在teardown_request进行remove
虽说Flask已经修正不需要捕获也可以,但是为了编码的优雅(暂时找不到好点的词),还是在dbsession_clean中进行了异常捕获。

@app.after_request
def after_clean(resp,*args,**kwargs):
    db.session.commit()
    return resp
@app.teardown_request
def dbsession_clean(exception=None):
    try:
        db.session.remove()
    finally:
        pass

第三种:使用自定义装饰器

def route(app_or_sub,rule,**options):
    def decorator(f):
        @wraps(f)
        def decorated_view(*args,**kwargs):
            res=f(*args,**kwargs)
            db.session.commit()
            return res
        endpoint = options.pop("endpoint", None)
        app_or_sub.add_url_rule(rule, endpoint, decorated_view, **options)
        return decorated_view
    return decorator

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/38270.html

相关文章

  • Flask 插件系列 - Flask-SQLAlchemy

    摘要:程序中最常用的莫过于关系型数据库了,也称数据库。对象是类的实例,表示程序使用的数据库。本文由发表于个人博客,采用自由转载保持署名非商用禁止演绎协议发布。非商业转载请注明作者及出处。本文标题为插件系列本文链接为更多阅读 简介 Web 开发中,一个重要的组成部分便是数据库了。Web 程序中最常用的莫过于关系型数据库了,也称 SQL 数据库。另外,文档数据库(如 mongodb)、键值对数据...

    LoftySoul 评论0 收藏0
  • flask-sqlalchemy操作(基础)

    摘要:以下内容介绍了的基础查询语句,下篇文章将介绍其高级查询聚合自关联连接子查询等模型类用户表地址信息关联表商品信息订单表增一对一构建对象添加对象提交事务一对多主表子表子表赋值对象添加提交多对多生成或获取商品对象生成订单对象订单表与商品表关联添加 以下内容介绍了Sqlalchemy的基础查询语句,下篇文章将介绍其高级查询(聚合、自关联、连接、子查询等) 模型类 # 用户表 class Use...

    Cciradih 评论0 收藏0
  • Flask Web Development —— 数据库(中)

    摘要:关系关系数据库通过使用关系在不同的表中建立连接。以下部分将介绍最常见的数据库操作。如果数据库已存在函数不会重新创建或更新数据库表。到目前为止对象只存于中,他们还没有被写入数据库。数据库会话也叫事务。删除行数据库会话同样有方法。 7、关系 关系数据库通过使用关系在不同的表中建立连接。图像5-1的关系图表达了用户和用户角色之间的简单关系。这个角色和用户是一对多关系,因为一个角色可以从属于...

    April 评论0 收藏0
  • Flask Web Development —— 数据库(中)

    摘要:关系关系数据库通过使用关系在不同的表中建立连接。以下部分将介绍最常见的数据库操作。如果数据库已存在函数不会重新创建或更新数据库表。到目前为止对象只存于中,他们还没有被写入数据库。数据库会话也叫事务。删除行数据库会话同样有方法。 7、关系 关系数据库通过使用关系在不同的表中建立连接。图像5-1的关系图表达了用户和用户角色之间的简单关系。这个角色和用户是一对多关系,因为一个角色可以从属于...

    keelii 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<