LoginSignup
2
0

More than 1 year has passed since last update.

SQLAlchemy flushとcommitの使い方

Last updated at Posted at 2022-07-03

まとめ(結論だけ知りたい人用)

以下の使い方をすると便利です。

  • 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が無いことで起こるバグ

以下のコードを実行してみます。

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クエリは以下の通りとなっています。

MySQLのログから抜粋
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が実行されるようなコードに修正する必要があります。

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()

このコードを実行した際のクエリは以下の通り

MySQLのログから抜粋
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が発行されます。

flushを明示的に実行した場合
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()

以下のクエリが発行されます。

MySQLのログから抜粋
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上で実行されます。

MySQLのログから抜粋
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のライブラリ側への委譲は有用であると考えられます。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0