1. 程式人生 > 其它 >面渣逆襲:二十二圖、八千字、二十問,徹底搞定MyBatis!

面渣逆襲:二十二圖、八千字、二十問,徹底搞定MyBatis!

大家好,我是老三,面渣逆襲系列繼續,這節我們的主角是MyBatis,作為當前國內最流行的ORM框架,是我們這些crud選手最趁手的工具,趕緊來看看面試都會問哪些問題吧。

基礎

1.說說什麼是MyBatis?

先吹一下

  • Mybatis 是一個半 ORM(物件關係對映)框架,它內部封裝了 JDBC,開發時只需要關注 SQL 語句本身,不需要花費精力去處理載入驅動、建立連線、建立statement 等繁雜的過程。程式設計師直接編寫原生態 sql,可以嚴格控制 sql 執行效能,靈活度高。

  • MyBatis 可以使用 XML 或註解來配置和對映原生資訊,將 POJO 對映成資料庫中的記錄,避免了幾乎所有的 JDBC 程式碼和手動設定引數以及獲取結果集。

再說一下缺點

  • SQL語句的編寫工作量較大,尤其當欄位多、關聯表多時,對開發人員編寫SQL語句的功底有一定要求
  • SQL語句依賴於資料庫,導致資料庫移植性差,不能隨意更換資料庫

ORM是什麼?

  • ORM(Object Relational Mapping),物件關係對映,是一種為了解決關係型資料庫資料與簡單Java物件(POJO)的對映關係的技術。簡單來說,ORM是通過使用描述物件和資料庫之間對映的元資料,將程式中的物件自動持久化到關係型資料庫中。

為什麼說Mybatis是半自動ORM對映工具?它與全自動的區別在哪裡?

  • Hibernate屬於全自動ORM對映工具,使用Hibernate查詢關聯物件或者關聯集合物件時,可以根據物件關係模型直接獲取,所以它是全自動的。
  • 而Mybatis在查詢關聯物件或關聯集合物件時,需要手動編寫SQL來完成,所以,被稱之為半自動ORM對映工具。

JDBC程式設計有哪些不足之處,MyBatis是如何解決的?

  • 1、資料連線建立、釋放頻繁造成系統資源浪費從而影響系統性能
    • 解決:在mybatis-config.xml中配置資料鏈接池,使用連線池統一管理資料庫連線。
  • 2、sql語句寫在程式碼中造成程式碼不易維護
    • 解決:將sql語句配置在XXXXmapper.xml檔案中與java程式碼分離。
  • 3、向sql語句傳引數麻煩,因為sql語句的where條件不一定,可能多也可能少,佔位符需要和引數一一對應。
    • 解決: Mybatis自動將java物件對映至sql語句。
  • 4、對結果集解析麻煩,sql變化導致解析程式碼變化,且解析前需要遍歷,如果能將資料庫記錄封裝成pojo物件解析比較方便。
    • 解決:Mybatis自動將sql執行結果對映至java物件。

2.Hibernate 和 MyBatis 有什麼區別?

PS:直接用Hibernate的應該不多了吧,畢竟大家都是“敏捷開發”,但架不住面試愛問。

相同點

  • 都是對jdbc的封裝,都是應用於持久層的框架。

不同點

  • 對映關係

    • MyBatis 是一個半自動對映的框架,配置Java物件與sql語句執行結果的對應關係,多表關聯關係配置簡單
    • Hibernate 是一個全表對映的框架,配置Java物件與資料庫表的對應關係,多表關聯關係配置複雜
  • SQL優化和移植性

    • Hibernate 對SQL語句封裝,提供了日誌、快取、級聯(級聯比 MyBatis 強大)等特性,此外還提供 HQL(Hibernate Query Language)操作資料庫,資料庫無關性支援好,但會多消耗效能。如果專案需要支援多種資料庫,程式碼開發量少,但SQL語句優化困難。
    • MyBatis 需要手動編寫 SQL,支援動態 SQL、處理列表、動態生成表名、支援儲存過程。開發工作量相對大些。直接使用SQL語句操作資料庫,不支援資料庫無關性,但sql語句優化容易。

MyBatis和Hibernate的適用場景?

  • Hibernate 是標準的ORM框架,SQL編寫量較少,但不夠靈活,適合於需求相對穩定,中小型的軟體專案,比如:辦公自動化系統

  • MyBatis 是半ORM框架,需要編寫較多SQL,但是比較靈活,適合於需求變化頻繁,快速迭代的專案,比如:電商網站

3.MyBatis使用過程?生命週期?

MyBatis基本使用的過程大概可以分為這麼幾步:

  • 1、 建立SqlSessionFactory

    可以從配置或者直接編碼來建立SqlSessionFactory

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  • 2、 通過SqlSessionFactory建立SqlSession

    SqlSession(會話)可以理解為程式和資料庫之間的橋樑

SqlSession session = sqlSessionFactory.openSession();
  • 3、 通過sqlsession執行資料庫操作

    • 可以通過 SqlSession 例項來直接執行已對映的 SQL 語句:

      Blog blog = (Blog)session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
      
    • 更常用的方式是先獲取Mapper(對映),然後再執行SQL語句:

      BlogMapper mapper = session.getMapper(BlogMapper.class);
      Blog blog = mapper.selectBlog(101);
      
  • 4、 呼叫session.commit()提交事務

    如果是更新、刪除語句,我們還需要提交一下事務。

  • 5、 呼叫session.close()關閉會話

    最後一定要記得關閉會話。

MyBatis生命週期?

上面提到了幾個MyBatis的元件,一般說的MyBatis生命週期就是這些元件的生命週期。

  • SqlSessionFactoryBuilder

    一旦建立了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 例項的生命週期只存在於方法的內部。

  • SqlSessionFactory

    SqlSessionFactory 是用來建立SqlSession的,相當於一個數據庫連線池,每次建立SqlSessionFactory都會使用資料庫資源,多次建立和銷燬是對資源的浪費。所以SqlSessionFactory是應用級的生命週期,而且應該是單例的。

  • SqlSession

    SqlSession相當於JDBC中的Connection,SqlSession 的例項不是執行緒安全的,因此是不能被共享的,所以它的最佳的生命週期是一次請求或一個方法。

  • Mapper

    對映器是一些繫結對映語句的介面。對映器介面的例項是從 SqlSession 中獲得的,它的生命週期在sqlsession事務方法之內,一般會控制在方法級。

當然,萬物皆可整合Spring,MyBatis通常也是和Spring整合使用,Spring可以幫助我們建立執行緒安全的、基於事務的 SqlSession 和對映器,並將它們直接注入到我們的 bean 中,我們不需要關心它們的建立過程和生命週期,那就是另外的故事了。

ps:接下來看看Mybatis的基本使用,不會有人不會吧?不會吧!

4.在mapper中如何傳遞多個引數?

方法1:順序傳參法

public User selectUser(String name, int deptId);

<select id="selectUser" resultMap="UserResultMap">
    select * from user
    where user_name = #{0} and dept_id = #{1}
</select>
  • #{}裡面的數字代表傳入引數的順序。
  • 這種方法不建議使用,sql層表達不直觀,且一旦順序調整容易出錯。

方法2:@Param註解傳參法

public User selectUser(@Param("userName") String name, int @Param("deptId") deptId);

<select id="selectUser" resultMap="UserResultMap">
    select * from user
    where user_name = #{userName} and dept_id = #{deptId}
</select>
  • #{}裡面的名稱對應的是註解@Param括號裡面修飾的名稱。
  • 這種方法在引數不多的情況還是比較直觀的,(推薦使用)。

方法3:Map傳參法

public User selectUser(Map<String, Object> params);

<select id="selectUser" parameterType="java.util.Map" resultMap="UserResultMap">
    select * from user
    where user_name = #{userName} and dept_id = #{deptId}
</select>
  • #{}裡面的名稱對應的是Map裡面的key名稱。
  • 這種方法適合傳遞多個引數,且引數易變能靈活傳遞的情況。

方法4:Java Bean傳參法

public User selectUser(User user);

<select id="selectUser" parameterType="com.jourwon.pojo.User" resultMap="UserResultMap">
    select * from user
    where user_name = #{userName} and dept_id = #{deptId}
</select>
  • #{}裡面的名稱對應的是User類裡面的成員屬性。
  • 這種方法直觀,需要建一個實體類,擴充套件不容易,需要加屬性,但程式碼可讀性強,業務邏輯處理方便,推薦使用。(推薦使用)。

5.實體類屬性名和表中欄位名不一樣 ,怎麼辦?

  • 第1種: 通過在查詢的SQL語句中定義欄位名的別名,讓欄位名的別名和實體類的屬性名一致。

    <select id="getOrder" parameterType="int" resultType="com.jourwon.pojo.Order">
           select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};
    </select>
    
    
  • 第2種: 通過resultMap中的<result>來對映欄位名和實體類屬性名的一一對應的關係。

    <select id="getOrder" parameterType="int" resultMap="orderResultMap">
    	select * from orders where order_id=#{id}
    </select>
        
    <resultMap type="com.jourwon.pojo.Order" id="orderResultMap">
        <!–用id屬性來對映主鍵欄位–>
        <id property="id" column="order_id">
        <!–用result屬性來對映非主鍵欄位,property為實體類屬性名,column為資料庫表中的屬性–>
    	<result property ="orderno" column ="order_no"/>
    	<result property="price" column="order_price" />
    </reslutMap>
    

6.Mybatis是否可以對映Enum列舉類?

  • Mybatis當然可以對映列舉類,不單可以對映列舉類,Mybatis可以對映任何物件到表的一列上。對映方式為自定義一個TypeHandler,實現TypeHandler的setParameter()和getResult()介面方法。
  • TypeHandler有兩個作用,一是完成從javaType至jdbcType的轉換,二是完成jdbcType至javaType的轉換,體現為setParameter()和getResult()兩個方法,分別代表設定sql問號佔位符引數和獲取列查詢結果。

7.#{}和${}的區別?

  • #{}是佔位符,預編譯處理;${}是拼接符,字串替換,沒有預編譯處理。
  • Mybatis在處理#{}時,#{}傳入引數是以字串傳入,會將SQL中的#{}替換為?號,呼叫PreparedStatement的set方法來賦值。
  • #{} 可以有效的防止SQL注入,提高系統安全性;${} 不能防止SQL 注入
  • #{} 的變數替換是在DBMS 中;${} 的變數替換是在 DBMS 外

8.模糊查詢like語句該怎麼寫?

  • 1 ’%${question}%’ 可能引起SQL注入,不推薦
  • 2 "%"#{question}"%" 注意:因為#{…}解析成sql語句時候,會在變數外側自動加單引號’ ',所以這裡 % 需要使用雙引號" ",不能使用單引號 ’ ',不然會查不到任何結果。
  • 3 CONCAT(’%’,#{question},’%’) 使用CONCAT()函式,(推薦✨)
  • 4 使用bind標籤(不推薦)
<select id="listUserLikeUsername" resultType="com.jourwon.pojo.User">
&emsp;&emsp;<bind name="pattern" value="'%' + username + '%'" />
&emsp;&emsp;select id,sex,age,username,password from person where username LIKE #{pattern}
</select>

9.Mybatis能執行一對一、一對多的關聯查詢嗎?

當然可以,不止支援一對一、一對多的關聯查詢,還支援多對多、多對一的關聯查詢。

  • 一對一<association>

    比如訂單和支付是一對一的關係,這種關聯的實現:

    • 實體類:

      public class Order {
          private Integer orderId;
          private String orderDesc;
      
          /**
           * 支付物件
           */
          private Pay pay;
          //……
      }
      
    • 結果對映

      <!-- 訂單resultMap -->
      <resultMap id="peopleResultMap" type="cn.fighter3.entity.Order">
          <id property="orderId" column="order_id" />
          <result property="orderDesc" column="order_desc"/>
          <!--一對一結果對映-->
          <association property="pay" javaType="cn.fighter3.entity.Pay">
              <id column="payId" property="pay_id"/>
              <result column="account" property="account"/>
          </association>
      </resultMap>
      
    • 查詢就是普通的關聯查

          <select id="getTeacher" resultMap="getTeacherMap" parameterType="int">
              select * from order o 
               left join pay p on o.order_id=p.order_id
              where  o.order_id=#{orderId}
          </select>
      
  • 一對多<collection>

    比如商品分類和商品,是一對多的關係。

    • 實體類

      public class Category {
          private int categoryId;
          private String categoryName;
        
          /**
          * 商品列表
          **/
          List<Product> products;
          //……
      }
      
    • 結果對映

              <resultMap type="Category" id="categoryBean">
                  <id column="categoryId" property="category_id" />
                  <result column="categoryName" property="category_name" />
           
                  <!-- 一對多的關係 -->
                  <!-- property: 指的是集合屬性的值, ofType:指的是集合中元素的型別 -->
                  <collection property="products" ofType="Product">
                      <id column="product_id" property="productId" />
                      <result column="productName" property="productName" />
                      <result column="price" property="price" />
                  </collection>
              </resultMap>
      
      • 查詢

        查詢就是一個普通的關聯查詢

                <!-- 關聯查詢分類和產品表 -->
                <select id="listCategory" resultMap="categoryBean">
                    select c.*, p.* from category_ c left join product_ p on c.id = p.cid
                </select>  
        

​ 那麼多對一、多對多怎麼實現呢?還是利用<association>和<collection>,篇幅所限,這裡就不展開了。

10.Mybatis是否支援延遲載入?原理?

  • Mybatis支援association關聯物件和collection關聯集合物件的延遲載入,association指的就是一對一,collection指的就是一對多查詢。在Mybatis配置檔案中,可以配置是否啟用延遲載入lazyLoadingEnabled=true|false。
  • 它的原理是,使用CGLIB建立目標物件的代理物件,當呼叫目標方法時,進入攔截器方法,比如呼叫a.getB().getName(),攔截器invoke()方法發現a.getB()是null值,那麼就會單獨傳送事先儲存好的查詢關聯B物件的sql,把B查詢上來,然後呼叫a.setB(b),於是a的物件b屬性就有值了,接著完成a.getB().getName()方法的呼叫。這就是延遲載入的基本原理。
  • 當然了,不光是Mybatis,幾乎所有的包括Hibernate,支援延遲載入的原理都是一樣的。

11.如何獲取生成的主鍵?

  • 新增標籤中新增:keyProperty=" ID " 即可

    <insert id="insert" useGeneratedKeys="true" keyProperty="userId" >
        insert into user( 
        user_name, user_password, create_time) 
        values(#{userName}, #{userPassword} , #{createTime, jdbcType= TIMESTAMP})
    </insert>
    
  • 這時候就可以完成回填主鍵

    mapper.insert(user);
    user.getId;
    

12.MyBatis支援動態SQL嗎?

MyBatis中有一些支援動態SQL的標籤,它們的原理是使用OGNL從SQL引數物件中計算表示式的值,根據表示式的值動態拼接SQL,以此來完成動態SQL的功能。

![

  • if

    根據條件來組成where子句

    <select id="findActiveBlogWithTitleLike"
         resultType="Blog">
      SELECT * FROM BLOG
      WHERE state = ‘ACTIVE’
      <if test="title != null">
        AND title like #{title}
      </if>
    </select>
    
  • choose (when, otherwise)

    這個和Java 中的 switch 語句有點像

    <select id="findActiveBlogLike"
         resultType="Blog">
      SELECT * FROM BLOG WHERE state = ‘ACTIVE’
      <choose>
        <when test="title != null">
          AND title like #{title}
        </when>
        <when test="author != null and author.name != null">
          AND author_name like #{author.name}
        </when>
        <otherwise>
          AND featured = 1
        </otherwise>
      </choose>
    </select>
    
  • trim (where, set)

    • <where>可以用在所有的查詢條件都是動態的情況
    <select id="findActiveBlogLike"
         resultType="Blog">
      SELECT * FROM BLOG
      <where>
        <if test="state != null">
             state = #{state}
        </if>
        <if test="title != null">
            AND title like #{title}
        </if>
        <if test="author != null and author.name != null">
            AND author_name like #{author.name}
        </if>
      </where>
    </select>
    
    • <set> 可以用在動態更新的時候

      <update id="updateAuthorIfNecessary">
        update Author
          <set>
            <if test="username != null">username=#{username},</if>
            <if test="password != null">password=#{password},</if>
            <if test="email != null">email=#{email},</if>
            <if test="bio != null">bio=#{bio}</if>
          </set>
        where id=#{id}
      </update>
      
  • foreach

    看到名字就知道了,這個是用來迴圈的,可以對集合進行遍歷

    <select id="selectPostIn" resultType="domain.blog.Post">
      SELECT *
      FROM POST P
      <where>
        <foreach item="item" index="index" collection="list"
            open="ID in (" separator="," close=")" nullable="true">
              #{item}
        </foreach>
      </where>
    </select>
    

13.MyBatis如何執行批量操作?

第一種方法:使用foreach標籤

foreach的主要用在構建in條件中,它可以在SQL語句中進行迭代一個集合。foreach標籤的屬性主要有item,index,collection,open,separator,close。

  • item   表示集合中每一個元素進行迭代時的別名,隨便起的變數名;
  • index   指定一個名字,用於表示在迭代過程中,每次迭代到的位置,不常用;
  • open   表示該語句以什麼開始,常用“(”;
  • separator 表示在每次進行迭代之間以什麼符號作為分隔符,常用“,”;
  • close   表示以什麼結束,常用“)”。

在使用foreach的時候最關鍵的也是最容易出錯的就是collection屬性,該屬性是必須指定的,但是在不同情況下,該屬性的值是不一樣的,主要有以下3種情況:

  1. 如果傳入的是單引數且引數型別是一個List的時候,collection屬性值為list
  2. 如果傳入的是單引數且引數型別是一個array陣列的時候,collection的屬性值為array
  3. 如果傳入的引數是多個的時候,我們就需要把它們封裝成一個Map了,當然單引數也可以封裝成map,實際上如果你在傳入引數的時候,在MyBatis裡面也是會把它封裝成一個Map的,
    map的key就是引數名,所以這個時候collection屬性值就是傳入的List或array物件在自己封裝的map裡面的key

看看批量儲存的兩種用法:

<!-- MySQL下批量儲存,可以foreach遍歷 mysql支援values(),(),()語法 --> //推薦使用
<insert id="addEmpsBatch">
    INSERT INTO emp(ename,gender,email,did)
    VALUES
    <foreach collection="emps" item="emp" separator=",">
        (#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})
    </foreach>
</insert>
<!-- 這種方式需要資料庫連線屬性allowMutiQueries=true的支援
 如jdbc.url=jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true -->  
<insert id="addEmpsBatch">
    <foreach collection="emps" item="emp" separator=";">                                 
        INSERT INTO emp(ename,gender,email,did)
        VALUES(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})
    </foreach>
</insert>

第二種方法:使用ExecutorType.BATCH

  • Mybatis內建的ExecutorType有3種,預設為simple,該模式下它為每個語句的執行建立一個新的預處理語句,單條提交sql;而batch模式重複使用已經預處理的語句,並且批量執行所有更新語句,顯然batch效能將更優; 但batch模式也有自己的問題,比如在Insert操作時,在事務沒有提交之前,是沒有辦法獲取到自增的id,在某些情況下不符合業務的需求。

    具體用法如下:

    //批量儲存方法測試
    @Test  
    public void testBatch() throws IOException{
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        //可以執行批量操作的sqlSession
        SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
    
        //批量儲存執行前時間
        long start = System.currentTimeMillis();
        try {
            EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
            for (int i = 0; i < 1000; i++) {
                mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1"));
            }
    
            openSession.commit();
            long end = System.currentTimeMillis();
            //批量儲存執行後的時間
            System.out.println("執行時長" + (end - start));
            //批量 預編譯sql一次==》設定引數==》10000次==》執行1次   677
            //非批量  (預編譯=設定引數=執行 )==》10000次   1121
    
        } finally {
            openSession.close();
        }
    }
    
  • mapper和mapper.xml如下

    public interface EmployeeMapper {   
        //批量儲存員工
        Long addEmp(Employee employee);
    }
    
    <mapper namespace="com.jourwon.mapper.EmployeeMapper"
         <!--批量儲存員工 -->
        <insert id="addEmp">
            insert into employee(lastName,email,gender)
            values(#{lastName},#{email},#{gender})
        </insert>
    </mapper>
    

14.說說Mybatis的一級、二級快取?

  1. 一級快取: 基於 PerpetualCache 的 HashMap 本地快取,其儲存作用域為SqlSession,各個SqlSession之間的快取相互隔離,當 Session flush 或 close 之後,該 SqlSession 中的所有 Cache 就將清空,MyBatis預設開啟一級快取。

  2. 二級快取與一級快取其機制相同,預設也是採用 PerpetualCache,HashMap 儲存,不同之處在於其儲存作用域為 Mapper(Namespace),可以在多個SqlSession之間共享,並且可自定義儲存源,如 Ehcache。預設不開啟二級快取,要開啟二級快取,使用二級快取屬性類需要實現Serializable序列化介面(可用來儲存物件的狀態),可在它的對映檔案中配置。

原理

15.能說說MyBatis的工作原理嗎?

我們已經大概知道了MyBatis的工作流程,按工作原理,可以分為兩大步:生成會話工廠會話執行

MyBatis是一個成熟的框架,篇幅限制,這裡抓大放小,來看看它的主要工作流程。

構建會話工廠

構造會話工廠也可以分為兩步:

  • 獲取配置

    獲取配置這一步經過了幾步轉化,最終由生成了一個配置類Configuration例項,這個配置類例項非常重要,主要作用包括:

    • 讀取配置檔案,包括基礎配置檔案和對映檔案

    • 初始化基礎配置,比如MyBatis的別名,還有其它的一些重要的類物件,像外掛、對映器、ObjectFactory等等

    • 提供一個單例,作為會話工廠構建的重要引數

    • 它的構建過程也會初始化一些環境變數,比如資料來源

       public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
              SqlSessionFactory var5;
              //省略異常處理
                  //xml配置構建器
                  XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
                  //通過轉化的Configuration構建SqlSessionFactory
                  var5 = this.build(parser.parse());
       }
      
  • 構建SqlSessionFactory

    SqlSessionFactory只是一個介面,構建出來的實際上是它的實現類的例項,一般我們用的都是它的實現類DefaultSqlSessionFactory,

        public SqlSessionFactory build(Configuration config) {
            return new DefaultSqlSessionFactory(config);
        }
    

會話執行

會話執行是MyBatis最複雜的部分,它的執行離不開四大元件的配合:

  • Executor(執行器)

    Executor起到了至關重要的作用,SqlSession只是一個門面,相當於客服,真正幹活的是是Executor,就像是默默無聞的工程師。它提供了相應的查詢和更新方法,以及事務方法。

                Environment environment = this.configuration.getEnvironment();
                TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
                tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
                //通過Configuration建立executor
                Executor executor = this.configuration.newExecutor(tx, execType);
                var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
    
  • StatementHandler(資料庫會話器)

    StatementHandler,顧名思義,處理資料庫會話的。我們以SimpleExecutor為例,看一下它的查詢方法,先生成了一個StatementHandler例項,再拿這個handler去執行query。

         public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
            Statement stmt = null;
    
            List var9;
            try {
                Configuration configuration = ms.getConfiguration();
                StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
                stmt = this.prepareStatement(handler, ms.getStatementLog());
                var9 = handler.query(stmt, resultHandler);
            } finally {
                this.closeStatement(stmt);
            }
    
            return var9;
        }
    

    再以最常用的PreparedStatementHandler看一下它的query方法,其實在上面的prepareStatement已經對引數進行了預編譯處理,到了這裡,就直接執行sql,使用ResultHandler處理返回結果。

        public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
            PreparedStatement ps = (PreparedStatement)statement;
            ps.execute();
            return this.resultSetHandler.handleResultSets(ps);
        }
    
  • ParameterHandler (引數處理器)

    PreparedStatementHandler裡對sql進行了預編譯處理

        public void parameterize(Statement statement) throws SQLException {
            this.parameterHandler.setParameters((PreparedStatement)statement);
        }
    

    這裡用的就是ParameterHandler,setParameters的作用就是設定預編譯SQL語句的引數。

    裡面還會用到typeHandler型別處理器,對型別進行處理。

    public interface ParameterHandler {
        Object getParameterObject();
    
        void setParameters(PreparedStatement var1) throws SQLException;
    }
    
  • ResultSetHandler(結果處理器)

    我們前面也看到了,最後的結果要通過ResultSetHandler來進行處理,handleResultSets這個方法就是用來包裝結果集的。Mybatis為我們提供了一個DefaultResultSetHandler,通常都是用這個實現類去進行結果的處理的。

    public interface ResultSetHandler {
        <E> List<E> handleResultSets(Statement var1) throws SQLException;
    
        <E> Cursor<E> handleCursorResultSets(Statement var1) throws SQLException;
    
        void handleOutputParameters(CallableStatement var1) throws SQLException;
    }
    

    它會使用typeHandle處理型別,然後用ObjectFactory提供的規則組裝物件,返回給呼叫者。

整體上總結一下會話執行:

PS:以上原始碼分析比較簡單,在真正的原始碼大佬面前可能過不了關,有條件的建議Debug一下MyBatis的原始碼。

我們最後把整個的工作流程串聯起來,簡單總結一下:

  1. 讀取 MyBatis 配置檔案——mybatis-config.xml 、載入對映檔案——對映檔案即 SQL 對映檔案,檔案中配置了操作資料庫的 SQL 語句。最後生成一個配置物件。

  2. 構造會話工廠:通過 MyBatis 的環境等配置資訊構建會話工廠 SqlSessionFactory。

  3. 建立會話物件:由會話工廠建立 SqlSession 物件,該物件中包含了執行 SQL 語句的所有方法。

  4. Executor 執行器:MyBatis 底層定義了一個 Executor 介面來操作資料庫,它將根據 SqlSession 傳遞的引數動態地生成需要執行的 SQL 語句,同時負責查詢快取的維護。

  5. StatementHandler:資料庫會話器,串聯起引數對映的處理和執行結果對映的處理。

  6. 引數處理:對輸入引數的型別進行處理,並預編譯。

  7. 結果處理:對返回結果的型別進行處理,根據物件對映規則,返回相應的物件。

16.MyBatis的功能架構是什麼樣的?

我們一般把Mybatis的功能架構分為三層:

  • API介面層:提供給外部使用的介面API,開發人員通過這些本地API來操縱資料庫。介面層一接收到呼叫請求就會呼叫資料處理層來完成具體的資料處理。
  • 資料處理層:負責具體的SQL查詢、SQL解析、SQL執行和執行結果對映處理等。它主要的目的是根據呼叫的請求完成一次資料庫操作。
  • 基礎支撐層:負責最基礎的功能支撐,包括連線管理、事務管理、配置載入和快取處理,這些都是共用的東西,將他們抽取出來作為最基礎的元件。為上層的資料處理層提供最基礎的支撐。

17.為什麼Mapper介面不需要實現類?

四個字回答:動態代理,我們來看一下獲取Mapper的過程:

  • 獲取Mapper

    我們都知道定義的Mapper介面是沒有實現類的,Mapper對映其實是通過動態代理實現的。

    BlogMapper mapper = session.getMapper(BlogMapper.class);
    

    七拐八繞地進去看一下,發現獲取Mapper的過程,需要先獲取MapperProxyFactory——Mapper代理工廠。

        public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
            MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
            if (mapperProxyFactory == null) {
                throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
            } else {
                try {
                    return mapperProxyFactory.newInstance(sqlSession);
                } catch (Exception var5) {
                    throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
                }
            }
        }
    
    • MapperProxyFactory

      MapperProxyFactory的作用是生成MapperProxy(Mapper代理物件)。

    public class MapperProxyFactory<T> {
        private final Class<T> mapperInterface;
        ……
        protected T newInstance(MapperProxy<T> mapperProxy) {
            return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
        }
    
        public T newInstance(SqlSession sqlSession) {
            MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
            return this.newInstance(mapperProxy);
        }
    }
    

    這裡可以看到動態代理對介面的繫結,它的作用就是生成動態代理物件(佔位),而代理的方法被放到了MapperProxy中。

    • MapperProxy

    MapperProxy裡,通常會生成一個MapperMethod物件,它是通過cachedMapperMethod方法對其進行初始化的,然後執行excute方法。

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
            } catch (Throwable var5) {
                throw ExceptionUtil.unwrapThrowable(var5);
            }
        }
    
    • MapperMethod

      MapperMethod裡的excute方法,會真正去執行sql。這裡用到了命令模式,其實繞一圈,最終它還是通過SqlSession的例項去執行物件的sql。

        public Object execute(SqlSession sqlSession, Object[] args) {
              Object result;
              Object param;
              ……
              case SELECT:
                  if (this.method.returnsVoid() && this.method.hasResultHandler()) {
                      this.executeWithResultHandler(sqlSession, args);
                      result = null;
                  } else if (this.method.returnsMany()) {
                      result = this.executeForMany(sqlSession, args);
                  } else if (this.method.returnsMap()) {
                      result = this.executeForMap(sqlSession, args);
                  } else if (this.method.returnsCursor()) {
                      result = this.executeForCursor(sqlSession, args);
                  } else {
                      param = this.method.convertArgsToSqlCommandParam(args);
                      result = sqlSession.selectOne(this.command.getName(), param);
                      if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
                          result = Optional.ofNullable(result);
                      }
                  }
                  break;
                 ……
          }
      

18.Mybatis都有哪些Executor執行器?

Mybatis有三種基本的Executor執行器,SimpleExecutor、ReuseExecutor、BatchExecutor。

  • SimpleExecutor:每執行一次update或select,就開啟一個Statement物件,用完立刻關閉Statement物件。
  • ReuseExecutor:執行update或select,以sql作為key查詢Statement物件,存在就使用,不存在就建立,用完後,不關閉Statement物件,而是放置於Map<String, Statement>內,供下一次使用。簡言之,就是重複使用Statement物件。
  • BatchExecutor:執行update(沒有select,JDBC批處理不支援select),將所有sql都新增到批處理中(addBatch()),等待統一執行(executeBatch()),它快取了多個Statement物件,每個Statement物件都是addBatch()完畢後,等待逐一執行executeBatch()批處理。與JDBC批處理相同。

作用範圍:Executor的這些特點,都嚴格限制在SqlSession生命週期範圍內。

Mybatis中如何指定使用哪一種Executor執行器?

  • 在Mybatis配置檔案中,在設定(settings)可以指定預設的ExecutorType執行器型別,也可以手動給DefaultSqlSessionFactory的建立SqlSession的方法傳遞ExecutorType型別引數,如SqlSession openSession(ExecutorType execType)。
  • 配置預設的執行器。SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(prepared statements); BATCH 執行器將重用語句並執行批量更新。

外掛

19.說說Mybatis的外掛執行原理,如何編寫一個外掛?

外掛的執行原理?

Mybatis會話的執行需要ParameterHandler、ResultSetHandler、StatementHandler、Executor這四大物件的配合,外掛的原理就是在這四大物件排程的時候,插入一些我我們自己的程式碼。

Mybatis使用JDK的動態代理,為目標物件生成代理物件。它提供了一個工具類Plugin,實現了InvocationHandler介面。

使用Plugin生成代理物件,代理物件在呼叫方法的時候,就會進入invoke方法,在invoke方法中,如果存在簽名的攔截方法,外掛的intercept方法就會在這裡被我們呼叫,然後就返回結果。如果不存在簽名方法,那麼將直接反射呼叫我們要執行的方法。

如何編寫一個外掛?

我們自己編寫MyBatis 外掛,只需要實現攔截器介面 Interceptor (org.apache.ibatis. plugin Interceptor ),在實現類中對攔截物件和方法進行處理。

  • 實現Mybatis的Interceptor介面並重寫intercept()方法

    這裡我們只是在目標物件執行目標方法的前後進行了列印;

    public class MyInterceptor implements Interceptor {
        Properties props=null;
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.out.println("before……");
            //如果當前代理的是一個非代理物件,那麼就會呼叫真實攔截物件的方法
            // 如果不是它就會呼叫下個外掛代理物件的invoke方法
            Object obj=invocation.proceed();
            System.out.println("after……");
            return obj;
        }
    }
    
  • 然後再給外掛編寫註解,確定要攔截的物件,要攔截的方法

    @Intercepts({@Signature(
            type = Executor.class,  //確定要攔截的物件
            method = "update",        //確定要攔截的方法
            args = {MappedStatement.class,Object.class}   //攔截方法的引數
    )})
    public class MyInterceptor implements Interceptor {
        Properties props=null;
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.out.println("before……");
            //如果當前代理的是一個非代理物件,那麼就會呼叫真實攔截物件的方法
            // 如果不是它就會呼叫下個外掛代理物件的invoke方法
            Object obj=invocation.proceed();
            System.out.println("after……");
            return obj;
        }
    }
    
  • 最後,再MyBatis配置檔案裡面配置外掛

<plugins>
    <plugin interceptor="xxx.MyPlugin">
       <property name="dbType",value="mysql"/>
    </plugin>
</plugins>    

20.MyBatis是如何進行分頁的?分頁外掛的原理是什麼?

MyBatis是如何分頁的?

MyBatis使用RowBounds物件進行分頁,它是針對ResultSet結果集執行的記憶體分頁,而非物理分頁。可以在sql內直接書寫帶有物理分頁的引數來完成物理分頁功能,也可以使用分頁外掛來完成物理分頁。

分頁外掛的原理是什麼?

  • 分頁外掛的基本原理是使用Mybatis提供的外掛介面,實現自定義外掛,攔截Executor的query方法
  • 在執行查詢的時候,攔截待執行的sql,然後重寫sql,根據dialect方言,新增對應的物理分頁語句和物理分頁引數。
  • 舉例:select * from student,攔截sql後重寫為:select t.* from (select * from student) t limit 0, 10

可以看一下一個大概的MyBatis通用分頁攔截器:



參考

[1]. MyBatis面試題(2020最新版)

[2].mybatis官網

[3].《深入淺出MyBatis基礎原理與實戰》

[4].聊聊MyBatis快取機制

[5].《MyBatis從入門到精通》