Skip to content

Latest commit

 

History

History
142 lines (100 loc) · 15.1 KB

flask-sqlalchemy.md

File metadata and controls

142 lines (100 loc) · 15.1 KB

Flask-SQLAlchemy

  • Flask-SQLAlchemy 做為 Flask 的 extension,使能在 Flask 裡輕鬆使用 SQLAlchemy 0.8+。

參考資料:

db.session ??

  • Quickstart — Flask-SQLAlchemy Documentation (2.3)
    • 提到 db = SQLAlchemy(app) (傳 app 進去,應該是 app.config[...] 取組態的慣例) 及 db.session.commit() 的用法
    • Road to Enlightenment 提到 "a preconfigured scoped session called session",還強調 "don’t have to remove it at the end of the request, Flask-SQLAlchemy does that for you",只要做 commit 就好 => 向 Flask 註冊 teardown function。
  • create_scoped_session(options=None) - API — Flask-SQLAlchemy Documentation (2.3) 預設會利用的 Flask 的 app context stack identity 確保 scoped_session 會在進出 request/response cycle 時自動建立/刪除。
  • flask-sqlalchemy/__init__.py at master · mitsuhiko/flask-sqlalchemyinit_app() 裡利用 @app.teardown_appcontext 向 app 註冊 teardown function,如果有設定 SQLALCHEMY_COMMIT_ON_TEARDOWN 會先 commit (沒有錯誤的話),再呼叫 session.remove();為什麼有 exception 時不做 rollback? => Session.remove() 內部會做 rollback。
  • teardown_appcontext(f) - API — Flask Documentation (0.12)
    • 註冊一個在 application context 結束時會被呼叫的 function,但又補上一句 "These functions are TYPICALLY also called when the request context is popped" 是什麼意思?
    • "Since a request context typically also manages an application context it would also be called when you pop a request context." 這句話是說 request context 結束時也會呼叫? => 實驗發現,appcontext 或 request teardown function 在 request 結束時都會被呼叫,其間有什麼差異??
  • remove() - Contextual/Thread-local Sessions — SQLAlchemy 1.2 Documentation Session.remove() 除了呼叫 Session.close() 及釋放 transactional/connection resource,特別提到 "transactions specifically are rolled back"。

如何入門 Flask-SQLAlchemy?

SQLAlchemy(metadata=None) 的用法??

  • 之前同事用另一個 subproject 定義 model,搭配 Flask-SQLAlchemy 使用;平常 application 執行沒問題,但 testing 時 model 無法跟 Flask-SQLAlchemy 的 app.db.Model 串起來,導致 app.db.create_all() 無效? 為什麼平常沒問題,測試期間就有問題? 後來同事將 db = SQLAlchemy(app) 改為 db = SQLAlchemy(app, metadata=Base.metadata) 即可。
  • class flask_sqlalchemy.SQLAlchemy - API — Flask-SQLAlchemy Documentation (2.3) v2.1 才加上 metadata 參數 -- The metadata associated with db.Model。
  • 問題跟 metadata 有關是確定的,例如 Object Relational Tutorial — SQLAlchemy 就有提到 MetaData.create_all()

Isolation Level ??

疑難排解 {: #troubleshooting }

MySQL server has gone away

Road to Enlightment - Flask-SQLAlchemy 提到:

You have to commit the session, but you don’t have to remove it at the end of the request, Flask-SQLAlchemy does that for you.

原來 SQLAlchemy.init_app() 會利用 @app.teardown_appcontext 向 app 註冊 teardown function,如果有設定 SQLALCHEMY_COMMIT_ON_TEARDOWN 會先 commit (沒有錯誤的話),再呼叫 session.remove() (內部會做 rollback)。

@app.teardown_appcontext
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 context 的 teardown function 會在每個 request 結束時被呼叫,就像文件上所宣稱的:

Registers a function to be called when the application context ends. These functions are TYPICALLY also called when the request context is popped.

到這裡,我們可以排除因為沒有明確呼叫 Session.close() 導致 connection 沒有回到 connection pool 的可能性,因為 Flask-SQLAlchemy 會自動處理。問題回到,若是與 MySQL 間的連線真的這麼不穩定 (為什麼不穩定要另外找),我們能怎麼做?

面對 pool 裡的 connection 可能失效 (stale) 的問題,SQLAlchemy 支援 Pessimistic (悲觀) 與 Optimistic (樂觀) 兩種策略。所謂悲觀是指 "不看好連線穩定",所以要從 pool 拿出 (checkout) connection 前,會先送出一個 test statement 以確定 connection 是有用的 (viable),這個動作稱做 pre ping (SQLAlchemy 1.2+)。雖然 pre ping 會對 checkout process 增加一些 overhead,但這是最簡單且可以完全排除拿到 stale pooled connection 的方法,應用端也不用擔心如何從 stale connection 回復的問題。

不過很不幸地,Flask-SQLAlchemy 尚不支援 pre ping 的做法,所以只能退而使用相對樂觀的策略 -- Pool Recycle。按照 Flask-SQLAlchemy 官方文件的說法,遇到 MySQL 時會自動將 SQLALCHEMY_POOL_RECYCLE 設定成 2hr,雖然這低於 MySQL wait_time 預設的 8hr,若是 MySQL 的連線不穩定 (未超過 wait_timeout 就中斷),2hr 的設定就顯得有點長,設成每 5 分鐘 recycle 一次似乎也沒什麼不妥。


後來在 Provide a way to configure the SA engine · Issue #589 · mitsuhiko/flask-sqlalchemy 的對話中找到了解法 -- 覆寫 flask_sqlalchemy.SQLAlchemy.apply_pool_defaults() 並強制加上 pool_pre_ping 參數即可:

from flask_sqlalchemy import SQLAlchemy as SA

class SQLAlchemy(SA):
    def apply_pool_defaults(self, app, options):
        SA.apply_pool_defaults(self, app, options)
        options["pool_pre_ping"] = True

db = SQLAlchemy()

參考資料:

from flask_sqlalchemy import SQLAlchemy as SA

class SQLAlchemy(SA):
    def apply_pool_defaults(self, app, options):
        SA.apply_pool_defaults(self, app, options)
        options["pool_pre_ping"] = True

db = SQLAlchemy()
  - [flask\-sqlalchemy/\_\_init\_\_\.py at 50944e77522d4aa005fc3c833b5a2042280686d3 · mitsuhiko/flask\-sqlalchemy](/~https://github.com/mitsuhiko/flask-sqlalchemy/blob/50944e77522d4aa005fc3c833b5a2042280686d3/flask_sqlalchemy/__init__.py#L814) 按照 `_EngineConnector.get_engine()` 的實作,會先後呼叫 `SQLAlchemy.apply_pool_defaults()` 與 `SQLAlchemy.apply_driver_hacks()` 更新準備傳給 `create_engine()` 的 options -- `self._engine = rv = sqlalchemy.create_engine(info, **options)`。

Flask-SQLAlchemy:

參考資料 {: #reference }

手冊: