🐕

@Transactional(readonly=true)はselectにつける必要はあるか?

に公開

selectに@Transactional(readOnly = true)をつける意味はあるのか?

久しぶりにDBアクセスのあるリポジトリを担当したら、selectを実行するメソッドに@Transactional(readOnly = true) が付いてた。「selectにトランザクション?」と思ったので調べてまとめる。

前提

  • Spring Boot
  • MyBatis
  • Kotlin
  • Oracle

結論

Oracle,MyBatisを使っているなら、単純なSELECTに@Transactional(readOnly = true)を付ける意味はほとんどない。

@Transactionalとは

一言で言えば、トランザクション境界を定義するためのアノテーション。メソッドやクラスにつけて「どこからどこまでを1単位にするか」を指定する。

トランザクション境界とは

「どこからどこまでを1単位にするか」という範囲のこと。

トランザクションは一連の行為がすべて成功したらコミット、失敗したらロールバックする仕組みだ。その一連の行為の範囲がトランザクション境界。下の例だと①〜③を同じトランザクションにしているので、いずれかが失敗したら全体がロールバックされる。

@Transactional
fun registerOrder() {
    orderMapper.insertOrder()       // ①
    stockMapper.decreaseStock()     // ②
    paymentService.requestPayment() // ③
}

無駄に広く境界を取るとロック範囲や接続保持時間が増えるので、必要最小限にするのが原則。

readOnly = trueの働き

まずはオプションであるreadOnly = trueの必要性から考えていく。

readOnly = trueを指定するとSpring側では基本的に Connection.setReadOnly(true) を呼ぶなどして「読み取り専用のヒント」を渡す。ただしこれはあくまでヒントで、必ずDB側で読み取り専用トランザクションになるわけではない。

以下の公式ドキュメントや実装箇所からもわかる

Spring ソースの該当箇所(抜粋)

DBMS面の意義

Connection.setReadOnly(true)はJDBC経由でドライバに渡すヒントで、DB/ドライバによって解釈が異なる。Oracleは単にsetReadOnlyを呼んだだけで「read-onlyトランザクション」に切り替わることは多くない。押さえておく点は以下。

  • 単一ステートメントの単純なSELECTだけならトランザクション境界は不要。自動コミットで問題ない。
  • トランザクションが必要になるケース:
    • 複数のSELECTを同一スナップショットで読みたいとき(ステートメント間で同じ時点のデータを参照したい場合)
    • SELECT ... FOR UPDATEのように排他ロックを取りたいとき
    • SELECTの結果を条件にしてUPDATE/DELETEを行い、全体で原子性を確保したいとき
  • Oracleはstatement-level read consistencyを提供するが、複数ステートメント間で完全に同じスナップショットを期待するならトランザクション境界や分離レベルの調整が必要になる場合がある。

ORM面の意義

ORM(たとえばHibernate)を使う場合、readOnly = trueはパフォーマンス最適化につながることがある。
理由:

  • 永続化コンテキストがオブジェクトの変更を追跡(dirty checking)しており、トランザクション終了時に自動でflushを行う。readOnlyを指定するとflushの最小化やスナップショットの扱いで恩恵があるケースがある。

一方、MyBatisはORMではない(SQL Mapper)。そのためORMが提供する以下の機能はMyBatisにはない:

  • 永続化コンテキストによる変更検知(dirty checking)
  • トランザクション終了時の自動flush

結果としてMyBatisの単純なSELECTにreadOnly = trueを付けても、ORM側の最適化恩恵は期待できない。

上記の内容は下記のドキュメントと実装から確認できる。

抜粋コード:

// DataSourceUtils.prepareConnectionForTransaction から抜粋
if (definition != null && definition.isReadOnly()) {
    try {
        if (debugEnabled) {
            logger.debug("Setting JDBC Connection [" + con + "] read-only");
        }
        con.setReadOnly(true);
    }
    catch (SQLException | RuntimeException ex) {
        // ドライバ例外等は吸収して継続する実装が多い
    }
}

抜粋(Javadoc):

This just serves as a hint for the actual transaction subsystem; it will not necessarily cause failure of write access attempts. A transaction manager which cannot interpret the read-only hint will not throw an exception when asked for a read-only transaction but rather silently ignore the hint.

抜粋(意味合いの確認用):

@Override
public boolean isReadOnly() {
    return this.readOnly;
}

以上のことから、今回の前提だとreadonly = trueオプションは不要である。

select文にトランザクションを張るべきかどうか

では次に、そもそも単純なselectにトランザクションが必要な時とはどのようなときなのか?から@Transactional自体の必要性について考える。

同一スナップショットの保証が必要なとき、が典型的なケースだろう。
複数のSELECTを同一の時点で一貫して見たいときは、トランザクションを張る必要がある。

スナップショットとは

スナップショットは「ある時点のデータの見え方」を指すイメージ。イメージとしては次の通り。

  • スナップショット = データベースのその瞬間の写真
    「今この瞬間のテーブルの状態」を切り取ったものだと考えると分かりやすい。

  • なぜ重要か
    複数のSELECTを実行すると、それぞれが別の時点の写真を参照してしまうことがある。途中で別のトランザクションがデータを書き換えると、AのSELECTとBのSELECTで異なる「写真」が返り、結果的に整合性が崩れる。

実務だと、複数テーブルのselect結果を元にレポートを作るときなどがこのケースに該当する。

複数テーブルの集計を組み合わせて生成する場合、途中で運用が入ってデータが変わるとレポートが矛盾する。
テーブルAの集計時点とテーブルBの集計時点がずれて、売上合計が一致しないと言った問題が生じうるためselect文にもトランザクションを張る必要がある。

参考(公式ドキュメント・ソース)

Discussion