🐘

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内で行われたすべての変更をデータベースに永続化します。
  • 動作:
    1. 内部的にsession.flush()を呼び出し、保留中のすべての変更(addされたオブジェクトなど)をSQLに変換してデータベースに送信します。
    2. データベースに対してCOMMIT文を発行し、現在のトランザクションを完了させます。

commit()が成功すると、トランザクション内のすべての変更が確定し、他のトランザクションからも参照可能になります。

# new_userをaddした後...
session.commit()

# この時点で、データベースにUserレコードが永続化される

変更をDBに送信する: flush()

commit()がトランザクション全体の確定であるのに対し、flush()はより低いレベルの操作です。

session.flush()

  • 役割: Sessionが追跡している変更(Pendingなオブジェクト、変更された属性など)を、対応するSQL(INSERT, UPDATE, DELETE)に変換してデータベースに送信します。
  • 動作:
    • SQLをデータベースに送信しますが、トランザクションはコミットしません。変更はまだデータベースのトランザクションバッファ内にあり、永続化はされていません。
    • flush()を実行すると、データベース側で自動採番されたIDなどがオブジェクトに反映されます。

では、commit()が内部でflush()を呼んでくれるのに、なぜ明示的にflush()を呼び出す必要があるのでしょうか?主なユースケースは以下の通りです。

ユースケース1: INSERT後に自動採番IDを取得したい

Userテーブルにidという自動採番の主キーがあるとします。UserオブジェクトをaddしただけではidNoneですが、flushすることでINSERT文が発行され、データベースが割り当てたidUserオブジェクト内で参照できるようになります。

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

DBの状態をオブジェクトに反映する: refresh()

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をより効果的かつ安全に利用することができます。特に、flushrefreshは特定の状況で非常に強力なツールとなるため、その挙動を覚えておくと良いでしょう。

Discussion