1. 程式人生 > >通過專案逐步深入瞭解Mybatis(四)

通過專案逐步深入瞭解Mybatis(四)


延遲載入

什麼是延遲載入?

resultMap可以實現高階對映(使用association、collection實現一對一及一對多對映),association、collection具備延遲載入功能。
需求:
如果查詢訂單並且關聯查詢使用者資訊。如果先查詢訂單資訊即可滿足要求,當我們需要查詢使用者資訊時再查詢使用者資訊。把對使用者資訊的按需去查詢就是延遲載入。

延遲載入:先從單表查詢、需要時再從關聯表去關聯查詢,大大提高 資料庫效能,因為查詢單表要比關聯查詢多張錶速度要快。

開啟延遲載入開關

在mybatis核心配置檔案中配置:

lazyLoadingEnabled、aggressiveLazyLoading

設定項 描述 允許值 預設值
lazyLoadingEnabled 全域性性設定懶載入。如果設為‘false’,則所有相關聯的都會被初始化載入。 true | false false
aggressiveLazyLoading 當設定為‘true’的時候,懶載入的物件可能被任何懶屬性全部載入。否則,每個屬性都按需載入。 true | false true
<settings>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading"
value="false"/>
</settings>

使用 association 實現延遲載入

需求:查詢訂單並且關聯查詢使用者資訊

Mapper.xml

需要定義兩個 mapper 的方法對應的 statement。

1、只查詢訂單資訊

SQL 語句: select * from orders

在查詢訂單的 statement 中使用 association 去延遲載入(執行)下邊的 statement (關聯查詢使用者資訊)

<!--查詢訂單並且關聯查詢使用者資訊,關聯使用者資訊需要通過 association 延遲載入-->
    <select id
="findOrdersUserLazyLoading" resultMap="OrdersUserLazyLoadingResultMap">
select * from orders </select>

2、關聯查詢使用者資訊

通過上面查詢訂單資訊中的 user_id 來關聯查詢使用者資訊。使用 UserMapper.xml 中的 findUserById

SQL語句:select * from user where id = user_id

<select id="findUserById" parameterType="int" resultType="user">
        select * from user where id = #{value}
    </select>

上邊先去執行 findOrdersUserLazyLoading,當需要去查詢使用者的時候再去執行 findUserById ,通過 resultMap的定義將延遲載入執行配置起來。也就是通過 resultMap 去載入 UserMapper.xml 檔案中的 select = findUserById

延遲載入的 resultMap

<!--定義 關聯使用者資訊(通過 association 延遲載入)的resultMap-->
    <resultMap id="OrdersUserLazyLoadingResultMap" type="cn.zhisheng.mybatis.po.Orders">
        <!--對訂單資訊對映-->
        <id column="id" property="id"/>
        <result column="user_id" property="userId"/>
        <result column="number" property="number"/>
        <result column="createtime" property="createtime"/>
        <result column="note" property="note"/>
        <!-- 實現對使用者資訊進行延遲載入
        select:指定延遲載入需要執行的statement的id(是根據user_id查詢使用者資訊的statement)
        要使用userMapper.xml中findUserById完成根據使用者id(user_id)使用者資訊的查詢,如果findUserById不在本mapper中需要前邊加namespace
        column:訂單資訊中關聯使用者資訊查詢的列,是user_id
        關聯查詢的sql理解為:
            SELECT orders.*,
            (SELECT username FROM USER WHERE orders.user_id = user.id)username,
            (SELECT sex FROM USER WHERE orders.user_id = user.id)sex
            FROM orders-->
        <association property="user" javaType="cn.zhisheng.mybatis.po.User" select="cn.zhisheng.mybatis.mapper.UserMapper.findUserById" column="user_id">
        </association>
    </resultMap>

OrderMapperCustom.java

public List<Orders> findOrdersUserLazyLoading() throws Exception;

測試程式碼:

@Test
    public void testFindOrdersUserLazyLoading() throws Exception
    {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //建立OrdersMapperCustom物件,mybatis自動生成代理物件
        OrdersMapperCustom ordersMapperCustom = sqlSession.getMapper(OrdersMapperCustom.class);
        //查詢訂單資訊
        List<Orders> list = ordersMapperCustom.findOrdersUserLazyLoading();
        //遍歷所查詢的的訂單資訊
        for (Orders orders : list)
        {
            //查詢使用者資訊
            User user = orders.getUser();
            System.out.println(user);
        }
        sqlSession.close();
    }

測試結果:

整個延遲載入的思路:

1、執行上邊mapper方法(findOrdersUserLazyLoading),內部去呼叫cn.zhisheng.mybatis.mapper.OrdersMapperCustom 中的 findOrdersUserLazyLoading 只查詢 orders 資訊(單表)。

2、在程式中去遍歷上一步驟查詢出的 List,當我們呼叫 Orders 中的 getUser 方法時,開始進行延遲載入。

3、延遲載入,去呼叫 UserMapper.xml 中 findUserbyId 這個方法獲取使用者資訊。

思考:

不使用 mybatis 提供的 association 及 collection 中的延遲載入功能,如何實現延遲載入??

實現方法如下:

定義兩個mapper方法:

1、查詢訂單列表

2、根據使用者id查詢使用者資訊

實現思路:

先去查詢第一個mapper方法,獲取訂單資訊列表

在程式中(service),按需去呼叫第二個mapper方法去查詢使用者資訊。

總之:

使用延遲載入方法,先去查詢 簡單的 sql(最好單表,也可以關聯查詢),再去按需要載入關聯查詢的其它資訊。

一對多延遲載入

上面的那個案例是一對一延遲載入,那麼如果我們想一對多進行延遲載入呢,其實也是很簡單的。

一對多延遲載入的方法同一對一延遲載入,在collection標籤中配置select內容。

延遲載入總結:

作用:

當需要查詢關聯資訊時再去資料庫查詢,預設不去關聯查詢,提高資料庫效能。
只有使用resultMap支援延遲載入設定。

場合:

當只有部分記錄需要關聯查詢其它資訊時,此時可按需延遲載入,需要關聯查詢時再向資料庫發出sql,以提高資料庫效能。

當全部需要關聯查詢資訊時,此時不用延遲載入,直接將關聯查詢資訊全部返回即可,可使用resultType或resultMap完成對映。

查詢快取

什麼是查詢快取?

mybatis提供查詢快取,用於減輕資料壓力,提高資料庫效能。

mybaits提供一級快取,和二級快取。

  • 一級快取是SqlSession級別的快取。在操作資料庫時需要構造 sqlSession物件,在物件中有一個數據結構(HashMap)用於儲存快取資料。不同的sqlSession之間的快取資料區域(HashMap)是互相不影響的。
  • 二級快取是mapper級別的快取,多個SqlSession去操作同一個Mapper的sql語句,多個SqlSession可以共用二級快取,二級快取是跨SqlSession的。

    為什麼要用快取?

如果快取中有資料就不用從資料庫中獲取,大大提高系統性能。

一級快取

工作原理

第一次發起查詢使用者id為1的使用者資訊,先去找快取中是否有id為1的使用者資訊,如果沒有,從資料庫查詢使用者資訊。

得到使用者資訊,將使用者資訊儲存到一級快取中。

如果sqlSession去執行commit操作(執行插入、更新、刪除),清空SqlSession中的一級快取,這樣做的目的為了讓快取中儲存的是最新的資訊,避免髒讀。

第二次發起查詢使用者id為1的使用者資訊,先去找快取中是否有id為1的使用者資訊,快取中有,直接從快取中獲取使用者資訊。

一級快取測試

Mybatis 預設支援一級快取,不需要在配置檔案中配置。

所以我們直接按照上面的步驟進行測試:

//一級快取測試
    @Test
    public void  testCache1() throws Exception {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //建立UserMapper物件,mybatis自動生成代理物件
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        //查詢使用的是同一個session
        //第一次發起請求,查詢Id 為1的使用者資訊
        User user1 = userMapper.findUserById(1);
        System.out.println(user1);
        //第二次發起請求,查詢Id 為1的使用者資訊
        User user2 = userMapper.findUserById(1);
        System.out.println(user2);
        sqlSession.close();
    }

通過結果可以看出第二次沒有發出sql查詢請求,

所以我們需要在中間執行 commit 操作

//如果sqlSession去執行commit操作(執行插入、更新、刪除),
// 清空SqlSession中的一級快取,這樣做的目的為了讓快取中儲存的是最新的資訊,避免髒讀。
//更新user1的資訊,
user1.setUsername("李飛");
//user1.setSex("男");
//user1.setAddress("北京");
userMapper.updateUserById(user1);
//提交事務,才會去清空快取
sqlSession.commit();

測試

一級快取應用

正式開發,是將 mybatis 和 spring 進行整合開發,事務控制在 service 中。

一個 service 方法中包括很多 mapper 方法呼叫。

service{

     //開始執行時,開啟事務,建立SqlSession物件

     //第一次呼叫mapper的方法findUserById(1)

     //第二次呼叫mapper的方法findUserById(1),從一級快取中取資料

     //方法結束,sqlSession關閉

}

如果是執行兩次service呼叫查詢相同的使用者資訊,不走一級快取,因為session方法結束,sqlSession就關閉,一級快取就清空。

二級快取

原理

首先開啟mybatis的二級快取。

sqlSession1去查詢使用者id為1的使用者資訊,查詢到使用者資訊會將查詢資料儲存到二級快取中。

如果SqlSession3去執行相同 mapper下sql,執行commit提交,清空該 mapper下的二級快取區域的資料。

sqlSession2去查詢使用者id為1的使用者資訊,去快取中找是否存在資料,如果存在直接從快取中取出資料。

二級快取與一級快取區別,二級快取的範圍更大,多個sqlSession可以共享一個UserMapper的二級快取區域。

UserMapper有一個二級快取區域(按namespace分) ,其它mapper也有自己的二級快取區域(按namespace分)。

每一個namespace的mapper都有一個二快取區域,兩個mapper的namespace如果相同,這兩個mapper執行sql查詢到資料將存在相同的二級快取區域中。

開啟二級快取

mybaits的二級快取是mapper範圍級別,除了在SqlMapConfig.xml設定二級快取的總開關,還要在具體的mapper.xml中開啟二級快取

在 SqlMapConfig.xml 開啟二級開關

<!-- 開啟二級快取 -->
<setting name="cacheEnabled" value="true"/>

然後在你的 Mapper 對映檔案中新增一行: ,表示此 mapper 開啟二級快取。

呼叫 pojo 類實現序列化介面

二級快取需要查詢結果對映的pojo物件實現Java.io.Serializable介面實現序列化和反序列化操作(因為二級快取資料儲存介質多種多樣,在記憶體不一樣),注意如果存在父類、成員pojo都需要實現序列化介面。

public class Orders implements Serializable
public class User implements Serializable

測試

//二級快取測試
    @Test
    public void testCache2() throws Exception
    {
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        SqlSession sqlSession3 = sqlSessionFactory.openSession();


        //建立UserMapper物件,mybatis自動生成代理物件
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        //sqlSession1 執行查詢 寫入快取(第一次查詢請求)
        User user1 = userMapper1.findUserById(1);
        System.out.println(user1);
        sqlSession1.close();


        //sqlSession3  執行提交  清空快取
        UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
        User user3 = userMapper3.findUserById(1);
        user3.setSex("女");
        user3.setAddress("山東濟南");
        user3.setUsername("崔建");
        userMapper3.updateUserById(user3);
        //提交事務,清空快取
        sqlSession3.commit();
        sqlSession3.close();

        //sqlSession2 執行查詢(第二次查詢請求)
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        User user2 = userMapper2.findUserById(1);
        System.out.println(user2);
        sqlSession2.close();
   }

結果

useCache 配置

在 statement 中設定 useCache=false 可以禁用當前 select 語句的二級快取,即每次查詢都會發出sql去查詢,預設情況是true,即該sql使用二級快取。

<select id="findUserById" parameterType="int" resultType="user" useCache="false">

總結:針對每次查詢都需要最新的資料sql,要設定成useCache=false,禁用二級快取。

重新整理快取(清空快取)

在mapper的同一個namespace中,如果有其它insert、update、delete操作資料後需要重新整理快取,如果不執行重新整理快取會出現髒讀。

設定statement配置中的flushCache=”true” 屬性,預設情況下為true即重新整理快取,如果改成false則不會重新整理。使用快取時如果手動修改資料庫表中的查詢資料會出現髒讀。

如下:

<insert id="insetrUser" parameterType="cn.zhisheng.mybatis.po.User" flushCache="true">

一般下執行完commit操作都需要重新整理快取,flushCache=true表示重新整理快取,這樣可以避免資料庫髒讀。

Mybatis Cache引數

flushInterval(重新整理間隔)可以被設定為任意的正整數,而且它們代表一個合理的毫秒形式的時間段。預設情況是不設定,也就是沒有重新整理間隔,快取僅僅呼叫語句時重新整理。

size(引用數目)可以被設定為任意正整數,要記住你快取的物件數目和你執行環境的可用記憶體資源數目。預設值是1024。

readOnly(只讀)屬性可以被設定為true或false。只讀的快取會給所有呼叫者返回快取物件的相同例項。因此這些物件不能被修改。這提供了很重要的效能優勢。可讀寫的快取會返回快取物件的拷貝(通過序列化)。這會慢一些,但是安全,因此預設是false。

如下例子:

<cache  eviction="FIFO" flushInterval="60000"  size="512" readOnly="true"/>

這個更高階的配置建立了一個 FIFO 快取,並每隔 60 秒重新整理,存數結果物件或列表的 512 個引用,而且返回的物件被認為是隻讀的,因此在不同執行緒中的呼叫者之間修改它們會導致衝突。可用的收回策略有, 預設的是 LRU:

  1. LRU – 最近最少使用的:移除最長時間不被使用的物件。

  2. FIFO – 先進先出:按物件進入快取的順序來移除它們。

  3. SOFT – 軟引用:移除基於垃圾回收器狀態和軟引用規則的物件。

  4. WEAK – 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的物件。

Mybatis 整合 ehcache

ehcache 是一個分散式快取框架。

分佈快取

我們系統為了提高系統併發,效能、一般對系統進行分散式部署(叢集部署方式)

不使用分佈快取,快取的資料在各各服務單獨儲存,不方便系統 開發。所以要使用分散式快取對快取資料進行集中管理。

mybatis無法實現分散式快取,需要和其它分散式快取框架進行整合。

整合方法

mybatis 提供了一個二級快取 cache 介面(org.apache.ibatis.cache 下的 Cache),如果要實現自己的快取邏輯,實現cache介面開發即可。

import java.util.concurrent.locks.ReadWriteLock;
public interface Cache {
    String getId();
    void putObject(Object var1, Object var2);
    Object getObject(Object var1);
    Object removeObject(Object var1);
    void clear();
    int getSize();
    ReadWriteLock getReadWriteLock();
}

mybatis和ehcache整合,mybatis 和 ehcache 整合包中提供了一個 cache 介面的實現類(org.apache.ibatis.cache.impl 下的 PerpetualCache)。

package org.apache.ibatis.cache.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
public class PerpetualCache implements Cache {
    private String id;
    private Map<Object, Object> cache = new HashMap();
    public PerpetualCache(String id) {
        this.id = id;
    }
    public String getId() {
        return this.id;
    }
    public int getSize() {
        return this.cache.size();
    }
    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }
    public Object getObject(Object key) {
        return this.cache.get(key);
    }
    public Object removeObject(Object key) {
        return this.cache.remove(key);
    }
    public void clear() {
        this.cache.clear();
    }
    public ReadWriteLock getReadWriteLock() {
        return null;
    }
    public boolean equals(Object o) {
        if(this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else if(this == o) {
            return true;
        } else if(!(o instanceof Cache)) {
            return false;
        } else {
            Cache otherCache = (Cache)o;
            return this.getId().equals(otherCache.getId());
        }
    }
    public int hashCode() {
        if(this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else {
            return this.getId().hashCode();
        }
    }
}

通過實現 Cache 介面可以實現 mybatis 快取資料通過其它快取資料庫整合,mybatis 的特長是sql操作,快取資料的管理不是 mybatis 的特長,為了提高快取的效能將 mybatis 和第三方的快取資料庫整合,比如 ehcache、memcache、Redis等。

  • 引入依賴包

    ehcache-core-2.6.5.jarmybatis-ehcache-1.0.2.jar

  • 引入快取配置檔案

    classpath下新增:ehcache.xml

    內容如下:

    <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    <diskStore path="C:\JetBrains\IDEAProject\ehcache" />
    <defaultCache 
        maxElementsInMemory="1000" 
        maxElementsOnDisk="10000000"
        eternal="false" 
        overflowToDisk="false" 
        timeToIdleSeconds="120"
        timeToLiveSeconds="120" 
        diskExpiryThreadIntervalSeconds="120"
        memoryStoreEvictionPolicy="LRU">
    </defaultCache>
    </ehcache>

    屬性說明:

    • diskStore:指定資料在磁碟中的儲存位置。
    • defaultCache:當藉助 CacheManager.add(“demoCache”) 建立Cache時,EhCache 便會採用指定的的管理策略

    以下屬性是必須的:

    • maxElementsInMemory - 在記憶體中快取的element的最大數目
    • maxElementsOnDisk - 在磁碟上快取的element的最大數目,若是0表示無窮大
    • eternal - 設定快取的elements是否永遠不過期。如果為true,則快取的資料始終有效,如果為false那麼還要根據timeToIdleSeconds,timeToLiveSeconds判斷
    • overflowToDisk- 設定當記憶體快取溢位的時候是否將過期的element快取到磁碟上

    以下屬性是可選的:

    • timeToIdleSeconds - 當快取在EhCache中的資料前後兩次訪問的時間超過timeToIdleSeconds的屬性取值時,這些資料便會刪除,預設值是0,也就是可閒置時間無窮大
    • timeToLiveSeconds - 快取element的有效生命期,預設是0.,也就是element存活時間無窮大

      diskSpoolBufferSizeMB 這個引數設定DiskStore(磁碟快取)的快取區大小.預設是30MB.每個Cache都應該有自己的一個緩衝區.

    • diskPersistent- 在VM重啟的時候是否啟用磁碟儲存EhCache中的資料,預設是false。

    • diskExpiryThreadIntervalSeconds - 磁碟快取的清理執行緒執行間隔,預設是120秒。每個120s,相應的執行緒會進行一次EhCache中資料的清理工作
    • memoryStoreEvictionPolicy - 當記憶體快取達到最大,有新的element加入的時候, 移除快取中element的策略。預設是LRU(最近最少使用),可選的有LFU(最不常使用)和FIFO(先進先出)
  • 開啟ehcache快取

    EhcacheCache 是ehcache對Cache介面的實現;修改mapper.xml檔案,在cache中指定EhcacheCache。

    根據需求調整快取引數:

    <cache type="org.mybatis.caches.ehcache.EhcacheCache" > 
          <property name="timeToIdleSeconds" value="3600"/>
          <property name="timeToLiveSeconds" value="3600"/>
          <!-- 同ehcache引數maxElementsInMemory -->
        <property name="maxEntriesLocalHeap" value="1000"/>
        <!-- 同ehcache引數maxElementsOnDisk -->
          <property name="maxEntriesLocalDisk" value="10000000"/>
          <property name="memoryStoreEvictionPolicy" value="LRU"/>
      </cache>

測試 :(這命中率就代表成功將ehcache 與 mybatis 整合了)

應用場景

對於訪問多的查詢請求且使用者對查詢結果實時性要求不高,此時可採用 mybatis 二級快取技術降低資料庫訪問量,提高訪問速度,業務場景比如:耗時較高的統計分析sql、電話賬單查詢sql等。

實現方法如下:通過設定重新整理間隔時間,由 mybatis 每隔一段時間自動清空快取,根據資料變化頻率設定快取重新整理間隔 flushInterval,比如設定為30分鐘、60分鐘、24小時等,根據需求而定。

侷限性

mybatis 二級快取對細粒度的資料級別的快取實現不好,比如如下需求:對商品資訊進行快取,由於商品資訊查詢訪問量大,但是要求使用者每次都能查詢最新的商品資訊,此時如果使用 mybatis 的二級快取就無法實現當一個商品變化時只重新整理該商品的快取資訊而不重新整理其它商品的資訊,因為 mybaits 的二級快取區域以 mapper 為單位劃分,當一個商品資訊變化會將所有商品資訊的快取資料全部清空。解決此類問題需要在業務層根據需求對資料有針對性快取。