MyBatis 快取

LZC發表於2020-02-22

快取

使用快取的作用也是減少 Java 應用程式與資料庫的互動次數,從而提升程式的執行效率。比如第一次查詢出某個物件之後,MyBatis 會自動將其存入快取,當下一次查詢同一個物件時,就可以直接從快取中獲取,不必再次訪問資料庫了。

MyBatis 有兩種快取:一級快取和二級快取。

MyBatis 自帶一級快取,並且是無法關閉的,一直存在,一級快取的資料儲存在 SqlSession 中,即它的作用域是同一個 SqlSession,當使用同一個 SqlSession 物件執行查詢的時候,第一次的執行結果會自動存入 SqlSession 快取,第二次查詢時可以直接從快取中獲取。

但是如果是兩個 SqlSession 查詢兩次同樣的 SQL,一級快取不會生效,需要訪問兩次資料庫。

同時需要注意,為了保證資料的一致性,如果 SqlSession 執行了增加、刪除,修改操作,MyBatis 會自動清空 SqlSession 快取中儲存的資料。

一級快取不需要進行任何配置,可以直接使用。

MyBatis 二級快取是比一級快取作用域更大的快取機制,它是 Mapper 級別的,只要是同一個 Mapper,無論使用多少個 SqlSession 來操作,資料都是共享的,多個不同的 SqlSession 可以共用二級快取。

MyBatis 二級快取預設是關閉的,需要使用時可以透過配置手動開啟。

一級快取

實體類物件

public class User {
    private Integer id;
    private String username;
    private String userEmail;
    private String userCity;
    private Integer age;
}

定義介面方法

public User findById(Integer id);

介面對應的 Mapper.xml 定義如下所示

<select id="findById" resultType="com.example.mybatis.entity.User">
    select * from user where id = #{id}
</select>

測試

public class App {
    public static void main( String[] args ) {
        // 載入 MyBatis 配置檔案
        InputStream is = App.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
        // 獲取 SqlSession, 代表和資料庫的一次會話, 用完需要關閉
        // SqlSession 和Connection, 都是非執行緒安全的, 每次使用都應該去獲取新的物件
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        // 獲取實現介面的代理物件
        // UserMapper 並沒有實現類, 但是mybatis會為這個介面生成一個代理物件(將介面和xml繫結)
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(userMapper.findById(1).toString());
        System.out.println(userMapper.findById(1).toString());
        sqlSession.close();
    }
}

結果如下所示

[2020-02-22 15:01:53:387] ==>  Preparing: select * from user where id = ? 
[2020-02-22 15:01:53:455] ==> Parameters: 1(Integer)
[2020-02-22 15:01:53:541] <==      Total: 1
User{id=1, username='zhangsan', userEmail='zhangsan@qq.com', userCity='shenzheng', age=20}
User{id=1, username='zhangsan', userEmail='zhangsan@qq.com', userCity='shenzheng', age=20}

可以看到結果,執行了一次 SQL 語句,查詢出兩個物件,第一個物件是透過 SQL 查詢的,並儲存到快取中,第二個物件是直接從快取中獲取的。

一級快取是 SqlSession 級別的,所以 SqlSession 一旦關閉,快取也就不復存在了,修改程式碼,再次測試。

public class App {
    public static void main( String[] args ) {
        // 載入 MyBatis 配置檔案
        InputStream is = App.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
        // 獲取 SqlSession, 代表和資料庫的一次會話, 用完需要關閉
        // SqlSession 和Connection, 都是非執行緒安全的, 每次使用都應該去獲取新的物件
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        // 獲取實現介面的代理物件
        // UserMapper 並沒有實現類, 但是mybatis會為這個介面生成一個代理物件(將介面和xml繫結)
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(userMapper.findById(1).toString());
        sqlSession.close();

        sqlSession = sqlSessionFactory.openSession(true);
        userMapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(userMapper.findById(1).toString());
        sqlSession.close();
    }
}

結果如下所示

[2020-02-22 15:06:04:769] ==>  Preparing: select * from user where id = ? 
[2020-02-22 15:06:04:871] ==> Parameters: 1(Integer)
[2020-02-22 15:06:04:971] <==      Total: 1
User{id=1, username='zhangsan', userEmail='zhangsan@qq.com', userCity='shenzheng', age=20}
[2020-02-22 15:06:04:989] Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@32464a14]
[2020-02-22 15:06:04:989] Returned connection 843467284 to pool.
[2020-02-22 15:06:04:989] Opening JDBC Connection
[2020-02-22 15:06:04:989] Checked out connection 843467284 from pool.
[2020-02-22 15:06:04:990] ==>  Preparing: select * from user where id = ? 
[2020-02-22 15:06:04:991] ==> Parameters: 1(Integer)
[2020-02-22 15:06:05:000] <==      Total: 1
User{id=1, username='zhangsan', userEmail='zhangsan@qq.com', userCity='shenzheng', age=20}

可以看到,執行了兩次 SQL,在關閉 SqlSession、一級快取失效的情況下,可以啟用二級快取,實現提升效率的需求。

二級快取

MyBatis 可以使用自帶的二級快取,也可以使用第三方的 ehcache 二級快取。

先來使用 MyBatis 自帶的二級快取,具體步驟如下所示。

1.配置檔案中開啟二級快取

<configuration>
    ...
    <!-- 設定 settings -->
    <settings>
        <!--開啟自動駝峰命名規則(camel case)對映,即從經典資料庫列名 A_COLUMN 到經典 Java 屬性名 aColumn 的類似對映-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 開啟二級快取 -->
        <setting name="cacheEnabled" value="true"/>
    </settings>
    ...
</configuration>

2.在Mapper檔案中配置<cache></cache>

<?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.mybatis.mapper.UserMapper">
    <cache></cache>
    <!--
        可以設定useCache="false"來關閉該查詢語句的快取
    -->
    <select id="findById" resultType="com.example.mybatis.entity.User">
        select * from user where id = #{id}
    </select>
</mapper>

3.實體類實現 Serializable 介面

public class User implements Serializable{
    private Integer id;
    private String username;
    private String userEmail;
    private String userCity;
    private Integer age;
}

4.測試

public class App {
    public static void main( String[] args ) {
        // 載入 MyBatis 配置檔案
        InputStream is = App.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
        // 獲取 SqlSession, 代表和資料庫的一次會話, 用完需要關閉
        // SqlSession 和Connection, 都是非執行緒安全的, 每次使用都應該去獲取新的物件
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        // 獲取實現介面的代理物件
        // UserMapper 並沒有實現類, 但是mybatis會為這個介面生成一個代理物件(將介面和xml繫結)
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(userMapper.findById(1).toString());
        sqlSession.close();

        sqlSession = sqlSessionFactory.openSession(true);
        userMapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(userMapper.findById(1).toString());
        sqlSession.close();
    }
}

結果如下所示

[2020-02-22 15:16:12:887] ==>  Preparing: select * from user where id = ? 
[2020-02-22 15:16:13:023] ==> Parameters: 1(Integer)
[2020-02-22 15:16:13:095] <==      Total: 1
User{id=1, username='zhangsan', userEmail='zhangsan@qq.com', userCity='shenzheng', age=20}
[2020-02-22 15:16:13:122] Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2bbaf4f0]
[2020-02-22 15:16:13:122] Returned connection 733672688 to pool.
[2020-02-22 15:16:13:131] Cache Hit Ratio [com.example.mybatis.mapper.UserMapper]: 0.5
User{id=1, username='zhangsan', userEmail='zhangsan@qq.com', userCity='shenzheng', age=20}

可以看到,執行了一次 SQL,查詢出兩個物件,二級快取生效

ehcache 二級快取

1.新增 ehcache 相關依賴

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.2.0</version>
</dependency>

2.在 resources 路徑下建立 ehcache.xml

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
    <!--指定磁碟路徑-->
    <diskStore path="G:\test" />
    <defaultCache
            maxElementsInMemory="1000"
            maxElementsOnDisk="10000000"
            eternal="false"
            overflowToDisk="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
    </defaultCache>
</ehcache>

3.配置檔案開啟二級快取

<configuration>

    <!-- 設定settings -->
    <settings>
        <!-- 列印SQL -->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
        <!--開啟自動駝峰命名規則(camel case)對映,即從經典資料庫列名 A_COLUMN 到經典 Java 屬性名 aColumn 的類似對映-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 開啟二級快取 -->
        <setting name="cacheEnabled" value="true"/>
    </settings>

</configuration>

4.在Mapper檔案中配置

<?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.mybatis.mapper.UserMapper">
    <!--
        collection: 指定要遍歷的集合
            如果為Collection型別的,key為collection;
            如果為List型別的,key為list
            如果是陣列型別,key為array
        item: 將當前遍歷的元素賦值給指定的變數
        open: 給遍歷的結果新增一個開始字元
        close: 給遍歷的結果新增一個結束字元
        separator: 每個元素之間的分隔符
    -->
    <select id="getUsersByIds"
            resultType="com.example.mybatis.entity.User">
        select * from user
        where id in
        <foreach collection="list" item="id" open="(" close=")" separator=",">
            #{id}
        </foreach>
    </select>
    <!-- 開啟二級快取 -->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache" >
        <!-- 快取建立以後,最後一次訪問快取的時間至失效的時間間隔 -->
        <property name="timeToIdleSeconds" value="3600"/>
        <!-- 快取自建立時間起至失效的時間間隔-->
        <property name="timeToLiveSeconds" value="3600"/>
        <!-- 快取回收策略,LRU 移除近期最少使用的物件 -->
        <property name="memoryStoreEvictionPolicy" value="LRU"/>
    </cache>
    <!--
        可以設定useCache="false"來關閉該查詢語句的快取
    -->
    <select useCache="true" id="findById" resultType="com.example.mybatis.entity.User">
        select * from user where id = #{id}
    </select>
</mapper>

5.實體類不需要實現 Serializable 介面

6.測試

public class App {
    public static void main( String[] args ) {
        // 載入 MyBatis 配置檔案
        InputStream is = App.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
        // 獲取 SqlSession, 代表和資料庫的一次會話, 用完需要關閉
        // SqlSession 和Connection, 都是非執行緒安全的, 每次使用都應該去獲取新的物件
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        // 獲取實現介面的代理物件
        // UserMapper 並沒有實現類, 但是mybatis會為這個介面生成一個代理物件(將介面和xml繫結)
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(userMapper.findById(1).toString());
        sqlSession.close();

        sqlSession = sqlSessionFactory.openSession(true);
        userMapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(userMapper.findById(1).toString());
        sqlSession.close();
    }
}

結果如下所示

==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, username, user_email, user_city, age
<==        Row: 1, zhangsan, zhangsan@qq.com, shenzheng, 20
<==      Total: 1
User{id=1, username='zhangsan', userEmail='zhangsan@qq.com', userCity='shenzheng', age=20}
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@59662a0b]
Returned connection 1499867659 to pool.
Cache Hit Ratio [com.example.mybatis.mapper.UserMapper]: 0.5
User{id=1, username='zhangsan', userEmail='zhangsan@qq.com', userCity='shenzheng', age=20}

同樣執行一次 SQL,查詢出兩個物件,ehcache 二級快取生效。

總結

MyBatis 的快取分兩種:一級快取和二級快取,一級快取是 SqlSession 級別的,二級快取是 Mapper 級別的。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章