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


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

基礎

1.說說什么是MyBatis?

MyBatis logo

先吹一下

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

  • MyBatis 可以使用 XML 或注解來配置和映射原生信息,將 POJO 映射成數據庫中的記錄,避免了幾乎所有的 JDBC 代碼和手動設置參數以及獲取結果集。

再說一下缺點

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

ORM是什么?

ORM簡單示意圖

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

為什么說Mybatis是半自動ORM映射工具?它與全自動的區別在哪里?

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

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

JDBC編程的不足

  • 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的適用場景?

Mybatis vs Hibernate

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

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

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

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事務方法之內,一般會控制在方法級。

MyBatis主要組件生命周期

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

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

這個應該會

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

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語句該怎么寫?

concat拼接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能執行一對一、一對多的關聯查詢嗎?

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

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的功能。

MyBatis![

  • 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如何執行批量操作?

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默認打開一級緩存。

    Mybatis一級緩存

  2. 二級緩存與一級緩存其機制相同,默認也是采用 PerpetualCache,HashMap 存儲,不同之處在於其存儲作用域為 Mapper(Namespace),可以在多個SqlSession之間共享,並且可自定義存儲源,如 Ehcache。默認不打開二級緩存,要開啟二級緩存,使用二級緩存屬性類需要實現Serializable序列化接口(可用來保存對象的狀態),可在它的映射文件中配置。

Mybatis二級緩存示意圖

原理

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

我們已經大概知道了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最復雜的部分,它的運行離不開四大組件的配合:

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的源碼。

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

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功能架構

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

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

17.為什么Mapper接口不需要實現類?

四個字回答:動態代理,我們來看一下獲取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類型

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插件原理簡圖

Mybatis使用JDK的動態代理,為目標對象生成代理對象。它提供了一個工具類Plugin,實現了InvocationHandler接口。

Plugin中調用插件方法

使用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通用分頁攔截器:

Mybatis-通用分頁攔截器



參考

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

[2].mybatis官網

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

[4].聊聊MyBatis緩存機制

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM