sqlalchemy+flask scope
web application が application container 上で実行されるという構成はよくあるパターンになっている。flask であれば一つの python process 内に複数の wsgi/asgi アプリケーションが同居している状態が対応する。一方で sqlalchemy は connection pooling などの機能を持っていて、それはプロセス内で共有された状態になる。
アプリケーションごとに sql 接続を管理したいので、コンテナ内の application context と同期して動作する仕組みが欲しくなる。このため sqlalchemy には scoped_session という仕組みがある。registry を使って、プログラムのどこからアクセスしても同じ Session instance が取得できる(ドキュメント参照)。
ややこしいのは scoped_session(factory)
で返されるものも factory であるというところ。ドキュメントを注意深く読んで使い方を調べると、global な scoped_session
(=factory) を経由する、と記載されている。scoped_session は基本的には接続パラメータなどを保持した Session factory で、ついでに Session
のように振舞うことができて、Session
メソッド呼び出しは registry にある実体に貫通するようになっている、と理解するとよい。
flask-sqlalchemy
実際、flask-sqlalchemy では scoped_session が global scope からアクセスできるように構成されている。scoped_session は flask と関係なく生成し、後から init_app で割り付けることができる。面白いのは sqlalchemy.event の引数に、SomeSessionOrFactory
と指定されているものがあるところで、ORM event への登録に scoped_session が使える。つまり次のような順序で定義できる。
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import event
app = Flask(__name__)
db = SQLAlchemy()
print(type(db.session))
# <class 'sqlalchemy.orm.scoping.scoped_session'>
@event.listens_for(db.session, "before_commit")
def my_before_commit(session):
print("my_before_commit called")
db.init_app(app)
with app.app_context():
print(type(db.session()))
# <class 'sqlalchemy.orm.session.SignallingSession'>
内容的にはflask_sqlalchemy は次の二つの段で構成されている。
- scoped_session ident_func での Session instance の隔離
- Flask.config の DB 接続情報(
SQLALCHEMY_DATABASE_URI
あるいはSQLALCHEMY_BINDS
)を、current_app 経由で flask application context に応じて切り替える
結局 flask_sqlalchemy が application context ごとに切り替えるのは binds (engine, connector) になっている。一方でテーブル定義などは flask_sqlalchemy.SQLAlchemy インスタンスに格納される。init_app を複数回呼び出しているということは、つまりテーブル定義は app 間で共有された状態で、appごとに DB 接続先だけが切り替えられる状況になっている。
flask-sqlalchemy-session
flask-sqlalchemy よりもさらに疎結合を目指した flask-sqlalchemy-session というライブラリがある。疎結合になるぶん定義順序に気を使わないといけない。
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from flask import Flask
from flask_sqlalchemy_session import flask_scoped_session, current_session
print(type(current_session))
# <class 'werkzeug.local.LocalProxy'>
session = flask_scoped_session(sessionmaker())
print(type(session))
# <class 'flask_sqlalchemy_session.flask_scoped_session'>
@event.listens_for(session, "before_commit")
def before_commit1(session):
print("before_commit")
session.configure(bind=create_engine("sqlite://"))
app = Flask(__name__)
session.init_app(app)
with app.app_context():
print(type(session()))
# <class 'sqlalchemy.orm.session.Session'>
print(type(current_session))
# <class 'werkzeug.local.LocalProxy'>
print(type(current_session()))
# <class 'sqlalchemy.orm.session.Session'>
print(current_session.execute)
# <bound method execute of <flask_sqlalchemy_session.flask_scoped_session object at 0x7f38783bdf70>>
# XXX DONT DO THIS
@event.listens_for(current_session, "before_commit")
def before_commit2(session):
print("before_commit2")
registry pattern を使用するという意味では、sqlalchemy の scoped_session も、flask/werkzeug の LocalProxy も同じことを目指している。つまり、どちらを使っても同じことになる。但し書きを強いて書くならば:
-
current_session
は scoped_session に追加で、flask context の制限を付けたものになっている。 - sqlalchemy の各種 API には scoped_session を使う。werkzeug.LocalProxy はかなり条件を選ぶし、無理に使う必要はない。
-
current_session
は flask ハンドラ中でふわっと使う場面で良さがでる。
Discussion