最近在開發mdwiki的時候遇到這樣一個問題.Post is unbond to session.
我就好奇了git
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找到可疑點github
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源碼,還好這貨只有兩個文件,比較少。
有這麼一段:sql
# 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模塊就解決問題了flask
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...session
而後在官網看到這樣一段:app
Consider SQLALCHEMY_COMMIT_ON_TEARDOWN harmful and remove from docs.ide
什麼?考慮移除這一特性?
剛剛知道這麼方便的特性,準備用來着,就要被移除?更況且源碼中也沒有提示要移除啊。
實際上是這樣的,做者準備在3.0版本移除SQLALCHEMY_COMMIT_ON_TEARDOWN這一特性,目前自2.1之後從文檔中移除了相關介紹。函數
爲何?接下來總結下大神們的探討。post
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