SQLAlchemyのSessionとTransaction:add, flush, commit, refreshの使い分け
はじめに
SQLAlchemyは、Pythonでデータベースを扱う際の強力なORM(Object-Relational Mapper)です。その中でもSession
オブジェクトは、データベースとの対話の中心的な役割を担います。しかし、Session
が提供するadd()
, flush()
, commit()
, refresh()
といったメソッドは、一見すると似たような働きをするため、その違いや使い分けを正確に理解するのは難しいかもしれません。
特に、これらのメソッドがデータベースのトランザクションとどのように関連しているかを理解することは、データの一貫性を保ち、意図しないバグを防ぐ上で非常に重要です。
この記事では、SQLAlchemyのSession
におけるadd
, flush
, commit
, refresh
の各メソッドの役割を、トランザクションとの関連性を軸に、自分へのメモも兼ねて詳しく解説します。
SQLAlchemyのSessionとは?
Session
は、一言で言えば「データベースとの対話を行うための一時的な作業領域」です。ORMによってマッピングされたPythonオブジェクト(インスタンス)への変更を追跡し、それらの変更をデータベースに反映させる役割を持ちます。
重要なのは、Session
が行う操作の多くは、暗黙的または明示的にトランザクション内で実行されるという点です。トランザクションは、一連のデータベース操作を「すべて成功」か「すべて失敗」のどちらかに保証する、不可分な処理単位です。これにより、データベースの状態が中途半端になることを防ぎ、データの一貫性を保ちます。
SQLAlchemyでは、Session
が開始されると、通常は自動的に新しいトランザクションが開始されます。このトランザクションは、commit()
されることで永続化されるか、rollback()
されることで破棄されます。
add()
と commit()
基本の操作: まずは、最も基本的な操作から見ていきましょう。
session.add(instance)
-
役割: Pythonオブジェクトを
Session
の管理下に置きます。 -
動作:
add()
を呼び出した時点では、まだデータベースに対してINSERT
文などのSQLは発行されません。オブジェクトはセッション内で「Pending(保留)」状態となり、「次にflush
されるタイミングでデータベースに挿入されるべきオブジェクト」としてマークされるだけです。
from sqlalchemy.orm import Session
# engineは事前に作成済みとする
session = Session(engine)
new_user = User(name="Alice", fullname="Alice Smith")
session.add(new_user)
# この時点では、まだデータベースにUserレコードは存在しない
session.commit()
-
役割:
Session
内で行われたすべての変更をデータベースに永続化します。 -
動作:
- 内部的に
session.flush()
を呼び出し、保留中のすべての変更(add
されたオブジェクトなど)をSQLに変換してデータベースに送信します。 - データベースに対して
COMMIT
文を発行し、現在のトランザクションを完了させます。
- 内部的に
commit()
が成功すると、トランザクション内のすべての変更が確定し、他のトランザクションからも参照可能になります。
# new_userをaddした後...
session.commit()
# この時点で、データベースにUserレコードが永続化される
flush()
変更をDBに送信する: commit()
がトランザクション全体の確定であるのに対し、flush()
はより低いレベルの操作です。
session.flush()
-
役割:
Session
が追跡している変更(Pendingなオブジェクト、変更された属性など)を、対応するSQL(INSERT
,UPDATE
,DELETE
)に変換してデータベースに送信します。 -
動作:
- SQLをデータベースに送信しますが、トランザクションはコミットしません。変更はまだデータベースのトランザクションバッファ内にあり、永続化はされていません。
-
flush()
を実行すると、データベース側で自動採番されたIDなどがオブジェクトに反映されます。
では、commit()
が内部でflush()
を呼んでくれるのに、なぜ明示的にflush()
を呼び出す必要があるのでしょうか?主なユースケースは以下の通りです。
ユースケース1: INSERT後に自動採番IDを取得したい
User
テーブルにid
という自動採番の主キーがあるとします。User
オブジェクトをadd
しただけではid
はNone
ですが、flush
することでINSERT
文が発行され、データベースが割り当てたid
をUser
オブジェクト内で参照できるようになります。
session.add(new_user)
session.flush()
# flush後なので、DBで採番されたIDがnew_userオブジェクトに反映されている
print(f"New user ID: {new_user.id}")
# この後、new_user.idを使って別の関連オブジェクトを作成し、
# 同じトランザクション内で保存することができる
# ...
# すべての処理が終わったら、最後にcommitする
session.commit()
refresh()
DBの状態をオブジェクトに反映する: refresh()
は、オブジェクトの状態をデータベースの最新の状態に更新するためのメソッドです。
session.refresh(instance)
-
役割: 指定したオブジェクトの属性を、データベースから再度
SELECT
して取得した値で上書きします。 -
動作: オブジェクトを「Expired(期限切れ)」状態にし、即座に
SELECT
文を発行してオブジェクトの属性を更新します。
commit()
が成功すると、セッション内のオブジェクトはデフォルトでExpired状態になり、次回アクセス時に自動で最新化されるため、通常は手動でrefresh()
を呼ぶ必要はあまりありません。しかし、特定の状況下で非常に役立ちます。
ユースケース: DBのトリガーやデフォルト値で設定された値を取得したい
テーブルにcreated_at
というカラムがあり、データベースの機能(例: DEFAULT CURRENT_TIMESTAMP
)によって値が自動的に設定されるとします。flush
しただけでは、その値はPythonオブジェクトに反映されません。このような場合にrefresh
が役立ちます。
session.add(new_user)
session.flush()
# flushしただけでは、new_user.created_atはまだNoneかもしれない
# refreshすることで、DB側で設定された値を取得する
session.refresh(new_user)
print(f"Created at: {new_user.created_at}") # DBのタイムスタンプが表示される
session.commit()
トランザクションとの関係まとめ
これまでの内容を、一連のトランザクションの流れとして整理してみましょう。
from sqlalchemy.orm import Session
# engineは事前に作成済みとする
# autocommit=False (デフォルト) の場合
with Session(engine) as session:
# Sessionの開始とともに、トランザクションも開始される (BEGIN)
# 1. オブジェクトを作成し、セッションに追加 (add)
# - PythonオブジェクトがPending状態になる
# - DBにはまだ何も送信されない
user1 = User(name="Bob")
session.add(user1)
# 2. 変更をDBに送信 (flush)
# - "INSERT INTO users ..." がDBに送信される
# - user1.id のようなDBで生成された値がオブジェクトに反映される
# - トランザクションはまだコミットされていない
session.flush()
# 3. DBの最新状態でオブジェクトを更新 (refresh)
# - "SELECT * FROM users WHERE id=..." が発行される
# - DBのトリガーなどで更新された値がオブジェクトに反映される
session.refresh(user1)
# 4. トランザクションを確定 (commit)
# - 内部で再度flushが実行される(もし前回のflush後に変更があれば)
# - DBに "COMMIT" が発行される
# - これまでの変更がすべて永続化される
session.commit()
# withブロックを抜ける際に、セッションはクローズされる
明示的なトランザクション管理
より複雑なロジックでは、session.begin()
を使ってトランザクションブロックを明示的に管理することが推奨されます。これにより、エラー発生時に自動的にrollback()
が行われます。
try:
with session.begin():
# このブロック内での操作はすべて一つのトランザクションとして扱われる
order = Order(...)
session.add(order)
session.flush() # order.id を取得
item = OrderItem(order_id=order.id, ...)
session.add(item)
# ブロックを正常に抜けると、自動的にcommitされる
except:
# エラーが発生すると、自動的にrollbackされる
print("Transaction failed. Rolling back.")
raise
まとめ
SQLAlchemyのSession
における各メソッドの役割は以下の通りです。
メソッド | 役割 | SQL発行 | トランザクション | 主なユースケース |
---|---|---|---|---|
add() |
オブジェクトをセッション管理下に置く | しない | - | オブジェクトを永続化対象としてマークする |
flush() |
変更をSQLとしてDBに送信 | する | コミットしない | 自動採番IDの取得、DB制約の事前チェック |
commit() |
トランザクションを確定し、変更を永続化 | する | コミットする | 一連の処理が完了した時点でのデータ永続化 |
refresh() |
オブジェクトをDBの最新状態で更新 | する | - | DBトリガーやデフォルト値で設定された値の取得 |
これらのメソッドの違いと、トランザクションとの関連性を正しく理解することで、SQLAlchemyをより効果的かつ安全に利用することができます。特に、flush
とrefresh
は特定の状況で非常に強力なツールとなるため、その挙動を覚えておくと良いでしょう。
Discussion