まとめ(結論だけ知りたい人用)
以下の使い方をすると便利です。
- commit
- 一連のデータ操作の最後に一回だけ実行するというルールを守る
- flush
-
sessionmaker()
にautoflush=True
をセットし、ライブラリ側にflush処理を委譲する
-
便利な理由を理解したい方は記事本文をどうぞ
概要
SQLAlchemyは暫定的データ更新内容をDBに確定させるメソッドとして
- commit
- flush
を提供しています。
この記事では、この二つのメソッドの使い方を見ていきます。
性質の違い
commitとflushは以下の異なる性質を持っています。
-
commit
- データの更新内容の恒久的な確定
- ROLLBACK不可
-
flush
- データの更新内容の一時的な確定
- ROLLBACK可能
この性質の違いにより、データを確定させるという同じようなメソッドであっても、
副次的な意味が全く異なるものとなっています。
commitの使い方
使い方は下記の通りです。
- 一連のデータ操作の最後に一回だけ実行する
理由としてはcommitが複数箇所で実行されるとトランザクションのACID特性が破綻する
恐れがあるためです。
破綻する例は下記リンク先参照
SQLAlchemyにおけるcommitの誤用によるACID特性の破綻例
コードとしては以下のような使い方になります。
SessionClass = sessionmaker(engine, autoflush=False, autocommit=False)
session = SessionClass()
# ~~~ 一連のデータ操作 ~~~
session.commit() # 最後に一回だけcommitする
flushの使い方
使い方は以下の通りです。
- データの更新を明示的に行いたい場合に実行する
具体的な例が無いとわかりにくいため、実際にflushが必要となる場面を見ていきます。
flushが無いことで起こるバグ
以下のコードを実行してみます。
SessionClass = sessionmaker(engine, autoflush=False)
session = SessionClass()
user = User(user_id="1", name="Johndoe", email="johndoe@example.com")
session.add(user)
# session.flush()
print(session.query(User).filter(User.user_id == "1").one())
session.commit()
session.close()
実行するとsqlalchemy.exc.NoResultFound
が発生します。
コードを見る限りuser_id="1"のレコードが存在しているため、
NoResultFound
が発生するようには見えません。
実際に実行されるSQLクエリは以下の通りとなっています。
Query SELECT user.id AS user_id, user.name AS user_name, user.email AS user_email FROM user WHERE user.id = '1'
INSERTが実行されていません。
実はSQLAlchemyではflushかcommitが実行されないとINSERT/UPDATEといった
データ内容の変更を伴うクエリが実行されません。
そのためflushを実行し、INSERTが実行されるようなコードに修正する必要があります。
SessionClass = sessionmaker(engine, autoflush=False)
session = SessionClass()
user = User(user_id="1", name="Johndoe", email="johndoe@example.com")
session.add(user)
session.flush()
print(session.query(User).filter(User.user_id == "1").one())
session.commit()
session.close()
このコードを実行した際のクエリは以下の通り
Query INSERT INTO user (id, name, email) VALUES ('1', 'Johndoe', 'johndoe@example.com')
Query SELECT user.id AS user_id, user.name AS user_name, user.email AS user_email FROM user WHERE user.id = '1'
Query COMMIT
ちゃんとINSERTが実行され、sqlalchemy.exc.NoResultFound
が発生しなくなりました。
実務上でcommitとflushを使う
ここまでメソッド単体としてのcommitとflushの使い方を見てきました。
メソッドを明示的に実行するとコードとしては読みやすくなります。
しかし残念なことに実際にそれで実装すると、無駄なクエリが発行されてしまう
という問題が発生します。
そのため、実務では明示的実行以外の美味しい方法を使って問題を回避しなければいけません。
ここからは実務上での美味しい使い方を見ていきます。
commit
commitに関しては特に美味しい方法はありません。
記事の冒頭に記したメソッド単体での使い方と同じく
- 一連のデータ操作の最後に一回だけ実行する
というルールを守るだけです。
flush
凄く端的に言えば
- sessionmaker(autoflush=True)を使い、ライブラリ側にflush処理を委譲する。
というものになります。
flushを明示的に実行すると無駄なクエリが発生します。
例えば下記のようなコードでは無駄なINSERTが発行されます。
SessionClass = sessionmaker(engine, autoflush=False)
session = SessionClass()
user = User(user_id="1", name="Johndoe", email="johndoe@example.com")
user2 = User(user_id="2", name="Johndoe", email="johndoe@example.com")
session.add(user)
session.flush()
session.add(user2)
session.flush()
print(session.query(User).all())
session.commit()
session.close()
以下のクエリが発行されます。
Query INSERT INTO user (id, name, email) VALUES ('1', 'Johndoe', 'johndoe@example.com')
Query INSERT INTO user (id, name, email) VALUES ('2', 'Johndoe', 'johndoe@example.com')
Query SELECT user.id AS user_id, user.name AS user_name, user.email AS user_email FROM user
Query COMMIT
INSERTの部分で無駄なINSERTが発行されています。
この次にautoflush=True
を有効化した場合を見ていきましょう。
SessionClass = sessionmaker(engine, autoflush=True)
session = SessionClass()
user = User(user_id="1", name="Johndoe", email="johndoe@example.com")
user2 = User(user_id="2", name="Johndoe", email="johndoe@example.com")
session.add(user)
session.add(user2)
print(session.query(User).all())
session.commit()
session.close()
実行すると以下のクエリがDB上で実行されます。
Query INSERT INTO user (id, name, email) VALUES ('1', 'Johndoe', 'johndoe@example.com'),('2', 'Johndoe', 'johndoe@example.com')
Query SELECT user.id AS user_id, user.name AS user_name, user.email AS user_email FROM user
Query COMMIT
INSERT文が1つにまとめられていることが分かります。
サンプルは少ないですが、これらの事実からflushのライブラリ側への委譲は有用であると考えられます。