Mybatis學習記錄

weixin_34370347發表於2018-12-14

目錄

  1. mybatis的基本概念
  2. mybatis如何構建和執行的
  3. mybatis的快取
  4. mybatis的外掛系統
  5. mybatis的日誌系統
  6. mybatis用到的設計模式
  7. myabtis整合到spring
  8. mybatis整合springboot自動化配置

1. mybatis的基本概念

MyBatis 是一款優秀的持久層框架,它支援定製化 SQL、儲存過程以及高階對映。MyBatis 避免了幾乎所有的 JDBC 程式碼和手動設定引數以及獲取結果集。MyBatis 可以使用簡單的 XML 或註解來配置和對映原生資訊,將介面和 Java 的 POJOs(Plain Old Java Objects,普通的 Java物件)對映成資料庫中的記錄。

上面是mybatis官方介紹,從介紹我們可以得知mybatis有以下特點:

  • 它是一個持久化框架
  • 它支援sql、儲存過程、高階對映
  • 它支援手動設定引數並且分裝結果集
  • 它支援xml和註解兩種配置方式

以下為mybatis內的一些基本概念:

  • SqlSessionFactory:SqlSession類的工廠類
  • SqlSession:資料庫會話類,為使用者提供資料庫操作方法
  • Executor:資料庫操作的執行器,SqlSession通過Executor運算元據庫
  • MappedStatement:是一個sql操作的抽象
  • 對映介面:具體的業務模組介面,對映介面不需要有實現類,介面內定義了一些列方法,每個方法對應一個sql操作,方法名就是sql操作的id
  • 對映檔案:當配置方式為xml時,可以將sql寫在xml配置檔案中,一個對映檔案對應一個對映介面
  • Cache:mybatis內部快取實現
  • Configuration:全域性配置資訊(以及配置資訊解析的結果)存放處,該例項全域性共享,該例項是SqlSessionFactory的屬性

2. mybatis如何構建和執行的

那mybatis是如果構建和執行的呢,先看一個小例子(這裡以xml配置方式為例):

  1. 建立一個maven專案
  2. 引入mybatis和mysql連線工具依賴
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.4.5</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>6.0.6</version>
</dependency>
複製程式碼
  1. 編寫mybatis配置檔案
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="config.properties"/>
    <settings>
        <setting name="logImpl" value="LOG4J2"/>
        <!-- 關閉一級快取 -->
        <setting name="localCacheScope" value="STATEMENT"/>
    </settings>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>
複製程式碼
  1. 編寫對映介面
public interface UserMapper {
    List<Map> selectUser();
}
複製程式碼
  1. 編寫對映xml檔案(resources/mapper/UserMapper.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="test.demos.mybatis.UserMapper">
    <select id="selectUser" resultType="java.util.Map">
        select * from user
    </select>
</mapper>
複製程式碼
  1. 編寫啟動類
public class App {
    public static void main(String[] args) throws Exception {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();

        InputStream resource = Resources.getResourceAsStream("config.xml");

        SqlSessionFactory sessionFactory = factoryBuilder.build(resource);

        SqlSession sqlSession = sessionFactory.openSession();

        /* 這裡通過jdk的動態代理獲取UserMapper介面的代理類 */
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        List<Map> list = userMapper.selectUser();
        System.out.println(list.size());
    }
}
複製程式碼

以上就是搭建純mybatis執行環境的過程,程式配置過程不詳述,這裡說一下mybatis的啟動構建和執行過程。

  • 先是建立SqlSessionFactoryBuilder例項,改例項的唯一作用就是用來構建SqlSessionFactory的,一但建立了SqlSessionFactory例項SqlSessionFactoryBuilder例項就沒用了。構建SqlSessionFactory的過程如下:
    • 載入mybatis配置檔案

    • (XMLConfigBuilder.parse)解析配置檔案:解析過程是將xml配置檔案內的所有配置標籤都解析幷包括

      • <properties/>
      • <settings/>
      • <typeAliases/>
      • <plugins/>
      • <objectFactory/>
      • <objectWrapperFactory/>
      • <reflectorFactory/>
      • <environments/>
      • <databaseIdProvider/>
      • <typeHandlers/>
      • <mappers/>

      解析每個標籤呼叫不同的方法處理該標籤的配置,例如解析標籤是會把內配置的所有對映記錄解析將mapper記錄新增到Configuration的MapperRegistry中去,並且將對應mapper配置檔案裡的所有的sql操作解析成MapperStatement(XMLMapperBuilder.parse),同時也會解析resultMap和快取配置。

  • 解析xml配置檔案最終會將所有配置資訊放到Configuration例項中去,該例項是全域性共享的,後續獲取Mapper介面代理、獲取MapperStatement、獲取Executor都會從這個Configuration例項中獲取。
  • 解析完之後建立DefaultSqlSessionFactory例項,這裡建立DefaultSqlSessionFactory例項比價簡單就是呼叫一個引數為Configuration的建構函式即可,因為所有的資訊都已經存放到Configuration例項中去了
  • 獲取SqlSession會話物件,呼叫SqlSessionFactory.open()方法即可,該方法最終會呼叫SqlSessionFactory.openSessionFromDataSource方法根據Configuration配置資訊建立一個SqlSession例項。
  • 有了SqlSession例項後,獲取對映介面的代理類,例如這裡的sqlSession.getMapper(UserMapper.class),這裡其實就是通過jdk的動態代理獲取得到UserMapper介面的代理類,實際代理的InvocationHandler是MapperProxy,在MapperProxy.invoke方法中會攔截對映介面的方法呼叫,然後建立(可能會被快取)MapperMethod例項通過執行MapperMethod.execute方法執行sql操作,接著會呼叫SqlSession內的一系列方法如selectList、insert、query等,根據呼叫的介面和方法組合的全限定名例如:com.test.UserMapper.getUser來獲取MappedStatement,最後通過Executor來作sql的操作(當然其內部也有些封裝執行操作,詳情可看Executor的實現類BaseExecutor、CachingExecutor的原始碼)。
  • Executor執行sql的操作的過程,會將sql執行的結果例如是insert、update、delete操作會返回執行的影響的條數,如果是query操作會將結果封裝成對應的sql配置檔案配置的型別(如pojo型別、map、resultMap等)返回List或者單個物件並返回。這裡mybatis大量使用了範型。

以上就是Mybatis大致的啟動構建和執行過程,只能將主要的節點描述,很多細節還需閱讀原始碼。

下圖為mybatis啟動示意圖:

Mybatis學習記錄

3. mybatis的快取

mybatis內建了兩種快取,一種是一級快取(預設開啟),一種是二級快取(預設開啟),一級快取是會話級別的也就是一個SqlSession例項內的快取,而二級快取是namespace級別的,所謂namespace就是一個對映介面的範圍,也就是說如果開啟了二級快取那麼多個會話如果呼叫的是同一個對映介面那麼是有可能命中二級快取的。下面詳細描述。

  • 一級快取:在上一部分我們知道對於SqlSession裡的一系列操作方法,實際上最終會呼叫Executor執行器內的方法來進行sql操作,Executor在mybatis種提供了幾個實現類,在不開啟二級快取的情況下預設使用SimpleExecutor實現類,SimpleExecutor是整合的BaseExecutor抽象類,大部分的方法已在BaseExecutor實現,我們關注BaseExecutor,當作查詢操作的時候最終會執行BaseExecutor.query方法,在BaseExecutor類的152行有這樣的程式碼list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;這裡就是一級快取實現的地方,即一級快取是儲存在BaseExecutor內部屬性localCache中,而localCache其實就是個PerpetualCache而該類是mybatis快取的一個實現類,下鑽到PerpetualCache內可以發現其內部有個型別為Map的cache屬性其中key為CacheKey值就是查詢結果。當執行了update、commit等方法後一級快取會被清空。我們可以看到,一級快取只提供了簡單的快取更新的策略,如果使用一個SqlSession例項作同一個查詢不管查詢多少此其結果都不會變,這就有可能出現髒資料,所以需要斟酌使用一級快取,如果對資料實時性要求高可以在mybatis配置檔案配置標籤裡設定<setting name="localCacheScope" value="STATEMENT"/>來關閉一級快取。

  • 二級快取:二級快取是預設開啟的,如果要關閉可以在mybatis配置檔案配置標籤裡設定<setting name="cacheEnabled" value="false"/>,開啟二級快取後SqlSession內的Executor為CachingExecutor,實際CachingExecutor是使用裝飾器模式將包了一層,具體sql操作委託給其他的Executor執行(其實預設是委託給SimpleExecutor),CachingExecutor只做二級快取的處理。原始碼CachingExecutor第95行,在執行查詢之前先從MappedStatement中獲取cache(如果對應mapper對映檔案中未配置那麼此處的cache是空的,其實這裡的cache在mybatis啟動構建解析配置檔案的時候就已經建立好了,這個cache例項是和namespace一一對應的)。如果部位空那麼就從cache中獲取值。但是這裡不是直接從cache中獲取值而是通過CacheExecutor內部的TransactionalCacheManager來獲取,之所以這樣是為了保證事務成功或失敗後快取的正常儲存和清理。例如這裡如果開啟二級快取做一次查詢其實沒發真正儲存快取,此時快取是儲存在TransactionalCache中的,TransactionalCache內儲存了所有本次事務操作需有需要快取的值,只有呼叫SqlSession.commit方法後將commit傳遞到TransactionalCache.commit才能真正儲存快取到namespace的cache例項中。在作insert、update、delete時二級快取也會被清除,想比一級快取二級快取有淘汰策略,預設策略上LRU(淘汰最急最少使用),可以在對映配置檔案的配置標籤中自定義,除此之外還有:

    • FIFO:先進先出:按物件進入快取的順序來移除它們
    • SOFT:軟引用:移除基於垃圾回收器狀態和軟引用規則的物件
    • WEAK:弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的物件

    例如:

        <cache
        eviction="FIFO"
        flushInterval="60000"
        size="512"
        readOnly="true"/>
    複製程式碼
  • Cache:Cache是mybatis在一二級快取是對快取的抽象,Cache介面有一系列的實現類,這些實現類使用裝飾器模式來實現對不能快取功能的包裝和功能疊加。

4. mybatis的外掛系統

MyBatis 允許你在已對映語句執行過程中的某一點進行攔截呼叫。預設情況下,MyBatis 允許使用外掛來攔截的方法呼叫包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

以上是官方的對plugin的介紹,本質上plugin在sql操作的執行週期中作用的,可以作用的點包括Executor、ParameterHandler、ResultSetHandler、StatementHandler內部的一系列方法。mybatis通過動態代理實現對作用點前後的自定義操作。在Configuration中有個interceptorChain屬性,即外掛作用鏈,在Configuration中newParameterHandler、newResultSetHandler、newStatementHandler、newExecutor這些方法都會呼叫InterceptorChain.pluginAll方法通過動態代理的方式將每個外掛穿起來,生成外掛動態代理鏈是通過外掛工具類Plugin來實現,呼叫Plugin.wrap這個靜態方法來建立代理類,代理InvocationHandler類就是Plugin(Plugin本身實現了InvocationHandler介面),當然在建立外掛代理類的過程中還會判斷外掛類的簽名資訊即外掛類的@Intercepts註解配置資訊,該配置資訊裡配置了該外掛的作用點(實際上就是作用的函式呼叫點)。例如我們想把查詢出來為List<Map>型別的結果內部的Map欄位轉成駝峰形式(如:user_name轉成userName)我們可以使用外掛來實現。

@Intercepts({@Signature(
        type= ResultSetHandler.class,
        method = "handleResultSets",
        args = {Statement.class})})
public class MyPlugin implements Interceptor {
    @Override
    @SuppressWarnings("unchecked")
    public Object intercept(Invocation invocation) throws Throwable {
        List result = (List) invocation.proceed();
        if (result != null && result.size() > 0) {
            if (result.get(0) instanceof Map) {
                List reList = new ArrayList();
                for (Map el : (List<Map>) result) {
                    Map map = new HashMap();
                    for (String key : (Set<String>) el.keySet()) {
                        map.put(getCamelKey(key), el.get(key));
                    }
                    reList.add(map);
                }
                return reList;
            }
        }
        return result;
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {

    }
    private String getCamelKey(String key) {
        String[] split = key.split("_");
        String camelKey = "";
        for (int i = 0; i < split.length; i++) {
            if (i != 0) camelKey += split[i].substring(0, 1).toUpperCase() + split[i].substring(1, split[i].length());
            else camelKey += split[i];
        }
        return camelKey;
    }
}
複製程式碼

5. mybatis的日誌系統

Mybatis 的內建日誌工廠提供日誌功能,內建日誌工廠將日誌交給以下其中一種工具作代理:

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • Log4j
  • JDK logging

實際mybatis只提供了一個日誌工廠LogFactory,mybatis通過日誌工廠獲取日誌物件,mybatis本身不提供日誌實現,具體的日誌交給第三方日誌框架來作。可以在mybatis配置檔案配置具體日誌實現,我門以log4j2為例:

<configuration>
  <settings>
    <setting name="logImpl" value="LOG4J2"/>
  </settings>
</configuration>
複製程式碼

配置了mybatis的log實現以後,需要引入相對應的日誌依賴包。

<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-api</artifactId>
  <version>2.11.1</version>
</dependency>
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.11.1</version>
</dependency>
複製程式碼

然後配置日誌框架的配置檔案(每個日誌框架的配置不同這裡以log4j2為例)

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info" name="RoutingTest">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="debug">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>
複製程式碼

6. mybatis用到的設計模式

mybatis在實現的時候用了一些設計模式,如:

  • 裝飾器模式:在快取方面Cache快取介面的各個實現類通過裝飾器模式來實現快取的功能的疊加
  • 動態代理模式:在對映介面代理和外掛方面mybatis使用jdk的動態代理模式是為對映介面提供代理類,為外掛系統提供代理生成外掛鏈
  • 工廠模式:mybatis為每個對映介面生成一個代理工廠MapperProxyFactory,每次獲取對映介面代理是通過代理工廠獲取
  • 組合模式:SqlNode的各個子類使用組合模式實現sql拼接
  • 單例模式:如LogFacotry
  • 模版方法模式:如抽象類BaseExecutor和其子類就是用該模式。模板類定義一個操作中的演算法的骨架,而將一些步驟延遲到子類中。使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟。

7. myabtis整合到spring

mybatis整合到spring需要新增mybatis-spring依賴,這個依賴包是mybatis和spring對接依賴包。新增spring依賴和mybatis-spring依賴

<!-- spring -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.0.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>5.0.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.0.4.RELEASE</version>
</dependency>
<!-- mybatis-spring -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.1</version>
</dependency>
複製程式碼

配置spring

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 屬性掃描 -->
    <context:property-placeholder location="config.properties"/>
    <!-- 元件掃描 -->
    <context:component-scan base-package="test.demos.mybatis"/>

    <!-- 資料來源 -->
    <bean id="dataSource" class="com.mysql.cj.jdbc.MysqlDataSource">
        <property name="url" value="${url}"/>
        <property name="user" value="${username}"/>
        <property name="password" value="${password}"/>
        <property name="databaseName" value="test"/>
    </bean>

    <!-- 配置sqlSessionFactory工廠bean -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="configLocation" value="classpath:config.xml"/>
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 配置sqlSessionTemplate -->
    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg ref="sqlSessionFactory"/>
    </bean>

    <!-- 註冊掃描對映介面bean -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="test.demos.mybatis"/>
    </bean>
</beans>
複製程式碼

從spring配置可以知道mybatis-spring主要做了一下幾件事:

  • 配置sqlSessionFactory工廠bean,該bean是一個工廠bean(可以理解為這個工廠bean就是SqlSessionFactory的bean,當注入的時候工廠bean會自動點用getObject方法獲取得到SqlSessionFactory例項)
  • 配置sqlSessionTemplate會話模版,它是SqlSession的子類,它相當於全域性的會話代理類它內部也是通過代理的方式sql操作委託給別的SqlSession。因為它可以作為全域性的SqlSession所以它是執行緒安全的,之所以執行緒安全的是因為所有通過SqlSessionTemplate呼叫的諸如selectList、update的方法都會委託給SqlSessionTemplate內部的sqlSessionProxy,而sqlSessionProxy是一個SqlSession的代理,其InvocationHandler是SqlSessionInterceptor,在SqlSessionInterceptor.invoke中每次都會從TransactionSynchronizationManager中獲取SqlSession,而在TransactionSynchronizationManager中使用ThreadLocal實現執行緒安全。(這裡大概描述詳情看原始碼SqlSessionTemplate、SqlSessionUtils)
  • 註冊掃描對映介面bean:MapperScannerConfigurer實現了BeanDefinitionRegistryPostProcessor介面,在bean初始化的時候會呼叫postProcessBeanDefinitionRegistry,MapperScannerConfigurer.postProcessBeanDefinitionRegistry方法內就是掃描註冊對映介面bean的過程。掃描註冊對映介面後,才可以被注入到其他的Component中。

8. mybatis整合springboot自動化配置

mybatis整合springboot需要新增一個start

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>
複製程式碼

其實mybatis-spring-boot-starter只是個空的依賴,mybatis-spring-boot-starter依賴了mybatis-spring-boot-autoconfigure,主要的程式碼在這個自動化配置包裡。自動化配置依賴會讀取mybatis相關的配置屬性,然後自動配置我們上面提到的mybatis相關的元件。配置例子:

mybatis.mapper-locations=classpath:/mapper/**/*Mapper.xml
mybatis.typeAliasesPackage=com.test.*.model
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis.configuration.callSettersOnNulls=true
複製程式碼

這裡不將springboot相關內容,只做配置樣例介紹。