MyBatisにおけるキャッシュ機構と遅延読み込みの詳細設定

キャッシュ機能の仕組み

MyBatisは、パフォーマンス向上のために二種類のキャッシュを提供しています。一つはSqlSessionレベルで動作するローカルキャッシュ(1次キャッシュ)、もう一つはSqlSessionFactoryレベルで共有されるセカンドレベルキャッシュ(2次キャッシュ)です。

1次キャッシュの挙動

新しいSqlSessionが生成されると、自動的にそのセッションに紐付いた1次キャッシュが作成されます。同じセッション内で同一のクエリが実行された場合、2回目以降はデータベースにアクセスせず、キャッシュから結果を取得します。

以下の条件で1次キャッシュはクリアされます:

  • INSERT / UPDATE / DELETE 操作の実行時
  • トランザクションのコミットまたはロールバック時
  • セッションがクローズされた時
  • session.clearCache()メソッドが明示的に呼び出された時

テストコード例

@Test
public void testFirstLevelCache() {
    try (SqlSession session = MyBatisUtil.openSession()) {
        UserMapper mapper = session.getMapper(UserMapper.class);

        // 最初のクエリ → DBアクセス
        List<User> users1 = mapper.findByNameAndPhone("山田", null);
        users1.forEach(System.out::println);

        System.out.println("-------- 再度のクエリ --------");

        // 同一クエリ → キャッシュから取得(SQLログ非表示)
        List<User> users2 = mapper.findByNameAndPhone("山田", null);
        users2.forEach(System.out::println);
    }
}

1次キャッシュが無効になるケース

  1. 異なるSqlSessionを使用した場合
  2. クエリのパラメータが異なる場合(例:電話番号を追加)
  3. 中間で変更操作(INSERT/UPDATE/DELETE)を実行した場合
  4. clearCache()を手動で呼び出した場合

手動クリアのサンプル

List<User> result1 = mapper.findByNameAndPhone("山田", null);
session.clearCache(); // キャッシュ削除
List<User> result2 = mapper.findByNameAndPhone("山田", null); // 再度DBアクセス

2次キャッシュの設定方法

2次キャッシュはファクトリ単位で共有されるため、複数のSqlSession間でもデータを再利用できます。有効化には以下の手順が必要です。

  1. グローバル設定:mybatis-config.xmlでキャッシュ有効化
<settings>
  <setting name="cacheEnabled" value="true"/>
</settings>
  1. マッパーXMLにタグ追加
<mapper namespace="com.example.UserMapper">
  <cache/>
  <select id="findByNameAndPhone" resultMap="userResultMap">
    SELECT * FROM user WHERE name = #{name} AND phone = #{phone}
  </select>
</mapper>
  1. エンティティのシリアライズ対応
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // フィールド定義...
}

2次キャッシュの有効化タイミング

1次キャッシュ内のデータは、SqlSessionがcloseまたはcommitされた時点で2次キャッシュへ書き込まれます。したがって、セッションを閉じない限り2次キャッシュは利用できません。

動作確認テスト

@Test
public void testSecondLevelCache() {
    // セッション1:DBから取得し、close時に2次キャッシュに保存
    try (SqlSession session1 = MyBatisUtil.openSession()) {
        UserMapper mapper1 = session1.getMapper(UserMapper.class);
        mapper1.findByNameAndPhone("山田", null);
    }

    System.out.println("---------------");

    // セッション2:2次キャッシュから取得(SQL非実行)
    try (SqlSession session2 = MyBatisUtil.openSession()) {
        UserMapper mapper2 = session2.getMapper(UserMapper.class);
        mapper2.findByNameAndPhone("山田", null);
    }
}

キャッシュの検索順序

MyBatisは以下のようにキャッシュを参照します:

  1. まず2次キャッシュをチェック
  2. ヒットしなければ1次キャッシュを確認
  3. それでもなければデータベースを問い合わせ
  4. 取得後、セッション終了時に1次→2次キャッシュへ保存

遅延読み込み(Lazy Loading)の活用

関連データが多い場合、すべてを同時に読み込むのは非効率です。必要になった時点で関連情報を取得する「遅延読み込み」を使うことで、初期ロードの負荷を軽減できます。

設定方法

<settings>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="aggressiveLazyLoading" value="false"/>
</settings>

動作モードの違い

  • 即時読み込み:lazyLoadingEnabled=false。主クエリ実行と同時にすべての関連データを取得
  • 積極的遅延:aggressiveLazyLoading=true。主オブジェクトの任意のgetter呼び出しで全関連データをロード
  • 細粒度遅延:aggressiveLazyLoading=false。関連プロパティのgetter呼び出し時のみロード(推奨)

マッピング例(ネストクエリ)

<resultMap id="userWithOrders" type="User">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
  <collection 
      property="orders" 
      select="com.example.OrderMapper.selectByUserId"
      column="id"
      fetchType="lazy"/>
</resultMap>

テストコード

@Test
public void testLazyLoading() {
    try (SqlSession session = MyBatisUtil.openSession()) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        User user = mapper.findById(1L); // 主情報のみ取得

        // ここで初めてordersのクエリが発行される
        System.out.println(user.getOrders().size());
    }
}

fetchTypeによる個別制御

fetchType="eager"またはfetchType="lazy"を指定することで、特定の関連項目だけ読み込み戦略を変更できます。これはグローバル設定よりも優先されます。

アノテーションによるマッピング定義

簡単なクエリについては、XMLを使わずアノテーションで直接記述可能です。

public interface UserMapper {
    
    @Select("SELECT * FROM user WHERE id = #{id}")
    User findById(@Param("id") Long id);

    @Insert("INSERT INTO user(name, phone) VALUES(#{name}, #{phone})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(User user);

    @Update("UPDATE user SET name=#{name} WHERE id=#{id}")
    int update(User user);

    @Delete("DELETE FROM user WHERE id = #{id}")
    int deleteById(Long id);
}

ただし、複雑なJOINや<resultMap>が必要な場合はXMLを使った定義が適しています。アノテーションはシンプルなCRUD操作に向いています。

タグ: MyBatis キャッシュ 遅延読み込み SqlSession 2次キャッシュ

9月23日 00:02 投稿