🐾

Mybatisの2ndレベルキャッシュ2

2025/01/23に公開

概要

Mybatisの2ndレベルキャッシュはinsert、update、deleteが行われるとクリアされます(公式サイト)。この記事では、この動作の確認を行なっていきます。

この記事は以下の記事の続きの内容となります。
Mybatisの2ndレベルキャッシュ

確認環境

  • Java 21
  • Spring Boot 3.4.1
  • MyBatis Spring Boot Starter 3.0.4
  • PostgreSQL 16.4

確認方法

以下のようなフローのプログラムのログから、クエリが発行されることを確認します。

  1. selectを実行
  2. selectを再実行し2ndレベルキャッシュが使用されることを確認
  3. insert or update or deleteを実行
  4. selectを再実行し2ndレベルキャッシュが使用されないことを確認

依存関係

build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter:3.4.1")
    implementation("org.springframework.boot:spring-boot-starter-jdbc:3.4.1")
    implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4")
    implementation("org.postgresql:postgresql:42.7.4")
}

データベース

テーブル定義

テーブル
                     Table "public.example"
 Column |         Type          | Collation | Nullable | Default 
--------+-----------------------+-----------+----------+---------
 id     | character varying(4)  |           | not null | 
 memo   | character varying(50) |           |          | 

                     Table "public.example2"
 Column |         Type          | Collation | Nullable | Default 
--------+-----------------------+-----------+----------+---------
 id     | character varying(4)  |           | not null | 
 memo   | character varying(50) |           |          | 

データ

データ
Table "public.example"
  id  |   memo   
------+----------
 0001 | example1
 0002 | example2

Table "public.example2"
 id | memo 
----+------

確認プログラム

構成
com.example
  +- ExampleApplication.java
  +- ExampleApplicationRunner.java
  +- ExampleRepository.java
  +- ExampleMapper.java
  +- ExampleMapper.xml

ExampleApplication はアプリケーションのエントリポイントです。

ExampleApplication.java
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}

ExampleApplicationRunnerApplicationRunner を実装します。このクラスがアプリケーション起動時に呼び出されます。

ExampleApplicationRunner.java
package com.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class ExampleApplicationRunner implements ApplicationRunner {
    private final Logger log = LoggerFactory.getLogger(ExampleApplicationRunner.class);

    private ExampleRepository exampleRepository;

    public ExampleApplicationRunner(ExampleRepository exampleRepository) {
        this.exampleRepository = exampleRepository;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("データ取得");
        exampleRepository.get();
        log.info("2ndレベルキャッシュが使用されることの確認");
        exampleRepository.get();
        log.info("データ登録");
        exampleRepository.regist();
        log.info("2ndレベルキャッシュが使用されないことの確認");
        exampleRepository.get();
    }
}

ExampleRepository はデータソースへアクセスするためのクラスです。

ExampleRepository.java
package com.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class ExampleRepository {
    private final Logger log = LoggerFactory.getLogger(ExampleRepository.class);

    private ExampleMapper exampleMapper;

    public ExampleRepository(ExampleMapper exampleMapper) {
        this.exampleMapper = exampleMapper;
    }

    @Transactional
    public void get() {
        log.info("クエリ1");
        exampleMapper.get("0001");
    }

    @Transactional
    public void regist() {
        log.info("クエリ2");
        exampleMapper.regist("0003", "追加");
    }
}

ExampleMapper ではデータベース操作に対応するメソッドを宣言します。

ExampleMapper.java
package com.example;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface ExampleMapper {
    List<Map<String, Object>> get(@Param("id") String id);
    void regist(@Param("id") String id, @Param("memo") String memo);
}

ExampleMapper.xml にクエリを記述します。

ExampleMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.example.ExampleMapper">
    <cache/>
    <select id="get" resultType="hashmap">
        select * from example where id = #{id};
    </select>

    <insert id="regist">
        insert into example (id, memo) values(#{id}, #{memo});
    </insert>
</mapper>

また、localCacheScopeSTATEMENT に設定して1stレベルキャッシュが使用されないように設定します。

application.properties
mybatis.configuration.local-cache-scope=STATEMENT

プログラムの実行結果

結果
com.example.ExampleApplicationRunner     : データ取得
  -- 中略 --
com.example.ExampleRepository            : クエリ1
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@46aa712c]
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.0
o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@767764251 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] will be managed by Spring
com.example.ExampleMapper.get            : ==>  Preparing: select * from example where id = ?;
com.example.ExampleMapper.get            : ==> Parameters: 0001(String)
com.example.ExampleMapper.get            : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@46aa712c]
  -- 中略 --
com.example.ExampleApplicationRunner     : 2ndレベルキャッシュが使用されることの確認
o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [com.example.ExampleRepository.get]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@558455114 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@558455114 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] to manual commit
org.postgresql.jdbc.PgConnection         :   setAutoCommit = false
com.example.ExampleRepository            : クエリ1
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1f1e58ca]
o.apache.ibatis.io.SerialFilterChecker   : As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.5
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1f1e58ca]
  -- 中略 --
com.example.ExampleApplicationRunner     : データ登録
o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [com.example.ExampleRepository.regist]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@275609562 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@275609562 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] to manual commit
org.postgresql.jdbc.PgConnection         :   setAutoCommit = false
com.example.ExampleRepository            : クエリ2
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f9c5048]
o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@275609562 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] will be managed by Spring
com.example.ExampleMapper.regist         : ==>  Preparing: insert into example (id, memo) values(?, ?);
com.example.ExampleMapper.regist         : ==> Parameters: 0003(String), 追加(String)
com.example.ExampleMapper.regist         : <==    Updates: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f9c5048]
  -- 中略 --
com.example.ExampleApplicationRunner     : 2ndレベルキャッシュが使用されないことの確認
o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [com.example.ExampleRepository.get]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@923539816 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@923539816 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] to manual commit
org.postgresql.jdbc.PgConnection         :   setAutoCommit = false
com.example.ExampleRepository            : クエリ1
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2f0bfe17]
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.3333333333333333
o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@923539816 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] will be managed by Spring
com.example.ExampleMapper.get            : ==>  Preparing: select * from example where id = ?;
com.example.ExampleMapper.get            : ==> Parameters: 0001(String)
com.example.ExampleMapper.get            : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2f0bfe17]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2f0bfe17]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2f0bfe17]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2f0bfe17]

最初の データ取得 のときにクエリが発行され、2ndレベルキャッシュが使用されることの確認 のときには2ndレベルキャッシュが使用されていることが分かります。最後の 2ndレベルキャッシュが使用されないことの確認 のときには以下のようにログ出力されているためクエリが発行されていることが確認できました。

com.example.ExampleMapper.get            : ==> Parameters: 0001(String)
com.example.ExampleMapper.get            : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional 

insertが行われた場合、2ndレベルキャッシュは使用されずに改めてクエリが発行されることが分かりました。insertを行っている箇所をupdateやdeleteに置き換えても同様の結果が得られます。

上記の確認時はselect、insert共に同じ example テーブルに対するものとなっています。ここからは、selectとinsertを別テーブルに行った際にどう動くかを確認します。

確認プログラムの変更

確認プログラムの ExampleMapper.xml を以下のように書き換えます。insertを example2 テーブルへ行うように変更しました。

ExampleMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.example.ExampleMapper">
    <cache/>
    <select id="get" resultType="hashmap">
        select * from example where id = #{id};
    </select>

    <insert id="regist">
        insert into example2 (id, memo) values(#{id}, #{memo});
    </insert>
</mapper>

変更後のプログラムの実行結果

結果
com.example.ExampleApplicationRunner     : データ取得
  -- 中略 --
com.example.ExampleRepository            : クエリ1
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@46aa712c]
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.0
o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@767764251 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] will be managed by Spring
com.example.ExampleMapper.get            : ==>  Preparing: select * from example where id = ?;
com.example.ExampleMapper.get            : ==> Parameters: 0001(String)
com.example.ExampleMapper.get            : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@46aa712c]
  -- 中略 --
com.example.ExampleApplicationRunner     : 2ndレベルキャッシュが使用されることの確認
o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [com.example.ExampleRepository.get]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@558455114 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@558455114 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] to manual commit
org.postgresql.jdbc.PgConnection         :   setAutoCommit = false
com.example.ExampleRepository            : クエリ1
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1f1e58ca]
o.apache.ibatis.io.SerialFilterChecker   : As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.5
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1f1e58ca]
  -- 中略 --
com.example.ExampleApplicationRunner     : データ登録
o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [com.example.ExampleRepository.regist]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@275609562 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@275609562 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] to manual commit
org.postgresql.jdbc.PgConnection         :   setAutoCommit = false
com.example.ExampleRepository            : クエリ2
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f9c5048]
o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@275609562 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] will be managed by Spring
com.example.ExampleMapper.regist         : ==>  Preparing: insert into example2 (id, memo) values(?, ?);
com.example.ExampleMapper.regist         : ==> Parameters: 0003(String), 追加(String)
com.example.ExampleMapper.regist         : <==    Updates: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f9c5048]
  -- 中略 --
com.example.ExampleApplicationRunner     : 2ndレベルキャッシュが使用されないことの確認
o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [com.example.ExampleRepository.get]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@923539816 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@923539816 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] to manual commit
org.postgresql.jdbc.PgConnection         :   setAutoCommit = false
com.example.ExampleRepository            : クエリ1
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2f0bfe17]
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.3333333333333333
o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@923539816 wrapping org.postgresql.jdbc.PgConnection@2b6fcb9f] will be managed by Spring
com.example.ExampleMapper.get            : ==>  Preparing: select * from example where id = ?;
com.example.ExampleMapper.get            : ==> Parameters: 0001(String)
com.example.ExampleMapper.get            : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2f0bfe17]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2f0bfe17]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2f0bfe17]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2f0bfe17]

insertの後のselectのときにクエリが発行されていることが分かります。別テーブルにinsertが行われた場合に2ndレベルキャッシュが使用されないことが分かりました。

updateとdeleteでも同様の結果が得られます。

参考

  1. Mybatis.org (Mapper XML ファイル - chache)

Discussion