キャッシュ機能の仕組み
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次キャッシュが無効になるケース
- 異なる
SqlSessionを使用した場合 - クエリのパラメータが異なる場合(例:電話番号を追加)
- 中間で変更操作(INSERT/UPDATE/DELETE)を実行した場合
clearCache()を手動で呼び出した場合
手動クリアのサンプル
List<User> result1 = mapper.findByNameAndPhone("山田", null);
session.clearCache(); // キャッシュ削除
List<User> result2 = mapper.findByNameAndPhone("山田", null); // 再度DBアクセス
2次キャッシュの設定方法
2次キャッシュはファクトリ単位で共有されるため、複数のSqlSession間でもデータを再利用できます。有効化には以下の手順が必要です。
- グローバル設定:
mybatis-config.xmlでキャッシュ有効化
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
- マッパーXMLにタグ追加
<mapper namespace="com.example.UserMapper">
<cache/>
<select id="findByNameAndPhone" resultMap="userResultMap">
SELECT * FROM user WHERE name = #{name} AND phone = #{phone}
</select>
</mapper>
- エンティティのシリアライズ対応
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は以下のようにキャッシュを参照します:
- まず2次キャッシュをチェック
- ヒットしなければ1次キャッシュを確認
- それでもなければデータベースを問い合わせ
- 取得後、セッション終了時に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操作に向いています。