一、背景
1.1 傳統Mybatis的弊端
1.1.1 場景描述
假設有兩張表:一張商品表、一張訂單表,具體表的欄位如下:
現有如下需求:
-
分別根據id查詢商品表和訂單表所有資訊
-
根據支付狀態和通知狀態查詢訂單表資訊
-
對訂單表增加一個訂單狀態,根據訂單狀態查詢訂單資訊
1.1.2 需求
需求a:根據id查詢商品表:
@Select(" SELECT p.id ,p.name ,p.picture ,p.type ,p.price, p.type, p.time
FROM product p where id = #{id} ")
List<Product> getProductsBYId(@Param("id") Integer id);
根據id查詢訂單表所有資訊:
@Select(" SELECT o.id ,o.pay_no ,o.user_id ,o.product_id ,o.pay_price, o.num, o.pay_time, o.order_type, o.notif_type
FROM order o where id = #{id} ")
List<Order> getOrderBYId(@Param("id") Integer id);
需求b:根據支付狀態和通知狀態查詢訂單表資訊
@Select(" SELECT o.id ,o.pay_no ,o.user_id ,o.product_id ,o.pay_price, o.num, o.pay_time, o.order_type, o.notif_type
FROM order o where order_type= #{orderType} ")
List<Order> getOrderBYId(@Param("orderType") Integer orderType);
@Select(" SELECT o.id ,o.pay_no ,o.user_id ,o.product_id ,o.pay_price, o.num, o.pay_time, o.order_type, o.notif_type
FROM order o where notify_type= #{notifyType} ")
List<Order> getOrderBYId(@Param("notifyType") Integer notifyType);
需求c:對訂單表增加一個訂單狀態status,根據訂單狀態查詢訂單資訊。
傳統mybaits需要三步:首先需要在訂單表裡加個欄位,然後在訂單的實體類新增這個屬性,並且將所有dao層設計該狀態的的查詢sql都修改一遍,加上這個欄位。
1.1.3 上述方式有什麼問題呢?
需求a:對於不同的實體類,即使查詢的目的一致,仍然需要重複構造類似的sql語句,僅僅是表欄位和表資訊不同。
需求b:對於相似的查詢條件,針對某個單一場景必須構造不同的sql,造成sql語句的大量冗餘。
需求c:將dao層所有涉及到新增欄位的sql都需要修改一遍,這個過程比較繁瑣且容易出錯。
使用mybatis-plus就可以解決上述問題。
1.1.4 Myatis-plus的解決方案
首先讓ProductMapper和OrderMapper繼承BaseMapper類:
public interface ProductMapper extends BaseMapper<Product> {
}
public interface OrderMapper extends BaseMapper<Order> {
}
需求a:
分別根據id查詢商品表和訂單表:由於BaseMapper中提供了selectById的方法,可以直接根據具體業務場景,傳入指定的引數例如(id=1)即可。無需書寫具體的sql語句,至於sql自動生成的原理將在下面介紹;
productMapper.selectById(1);
orderMapper.selectById(1);
需求b:
此時利用BaseMapper.selectList(Wapper queryWrapper)方法直接構造查詢條件,例如查詢支付狀態為2和通知狀態為1的訂單資訊
orderMapper.selectList(new QueryWrapper<Order>().eq("orderType",2));
orderMapper.selectList(new QueryWrapper<Order>().eq("notifyType",1));
orderMapper.selectList(new QueryWrapper<Order>().eq("orderType",2));orderMapper.selectList(new QueryWrapper<Order>().eq("notifyType",1));
此時我們可以發現:使用了Mybatis-plus以後,我們更加聚焦於業務本身,對於上述相似的應用場景,無需構造雷同的SQL,利用包裝器直接傳入查詢條件。
需求c:
前兩步與傳統mybatis一致,由於MyBatis-plus無需手動建立SQL,因此減少了大量的重複勞動。
1.2 MyBatis-Plus的定位
MyBatis-Plus (opens new window)(簡稱 MP)是一個 MyBatis (opens new window)的增強工具,在 MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生。
1.3 特性
-
無侵入:只做增強不做改變,引入它不會對現有工程產生影響,如絲般順滑;
-
損耗小:啟動即會自動注入基本 CURD,效能基本無損耗,直接物件導向操作;
-
強大的 CRUD 操作:內建通用 Mapper、通用 Service,僅僅通過少量配置即可實現單表大部分 CRUD 操作,更有強大的條件構造器,滿足各類使用需求;
-
支援 Lambda 形式呼叫:通過 Lambda 表示式,方便的編寫各類查詢條件,無需再擔心欄位寫錯;
-
支援主鍵自動生成:支援多達 4 種主鍵策略(內含分散式唯一 ID 生成器 - Sequence),可自由配置,完美解決主鍵問題;
-
支援 ActiveRecord 模式:支援 ActiveRecord 形式呼叫,實體類只需繼承 Model 類即可進行強大的 CRUD 操作;
-
支援自定義全域性通用操作:支援全域性通用方法注入( Write once, use anywhere );
-
內建程式碼生成器:採用程式碼或者 Maven 外掛可快速生成 Mapper 、 Model 、 Service 、 Controller 層程式碼,支援模板引擎,更有超多自定義配置等您來使用;
-
內建分頁外掛:基於 MyBatis 物理分頁,開發者無需關心具體操作,配置好外掛之後,寫分頁等同於普通 List 查詢;
-
分頁外掛支援多種資料庫:支援 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多種資料庫;
-
內建效能分析外掛:可輸出 Sql 語句以及其執行時間,建議開發測試時啟用該功能,能快速揪出慢查詢;
-
內建全域性攔截外掛:提供全表 delete 、 update 操作智慧分析阻斷,也可自定義攔截規則,預防誤操作。
1.4 原理解析
本文針對MyBatis-plus中的核心功能:SQL自動注入功能,進行流程分析及原理探究。
二、準備工作
先從一個簡單的demo入手,感受一下MyBatis-plus的便捷性。
2.1 基礎介面BaseMapper
原始碼中提供了一個基礎介面,裡面包含了基本的增刪改查方法。
2.2 建立實體類物件
2.3 業務介面UserMapper
業務中根據具體實體物件,繼承該抽象介面。
2.4 測試用例
控制檯顯示:MyBatis-plus最終為我們自動生成了SQL語句。根據上述操作分析:UserMapper繼承了BaseMapper,擁有了deleteById的方法,但是MyBatis-plus是基於mybatis的增強版,關鍵在於最終仍然需要提供具體的SQL語句,來進行資料庫操作。
下面就通過debug由上而下分析mybatis-plus是如何生成業務sql以及自動注入的。
三、SQL語句儲存在哪裡?
mappedStatements:描述sql資訊
如下圖所示:mybatis為我們生成了一個代理物件,裡面包含了一些重要的屬性。
具體如下:
userMapper——>mybatisMapperProxy——>sqlSession——>sqlSessionFactory——>configuration——>mappedStatements——>mappedStatement——>sql語句
至此我們可以發現每一個SQL語句對應一個mappedStatement,mappedstatements儲存在configuration檔案(configuration是mybatis的全域性配置檔案,包含資料來源、mapper、其他配置資訊)中。
四、SQL語句是什麼時候注入的?
4.1 AbstractMethod.addMappedStatement
基於上面的分析,想要知道SQL語句什麼時候拿到的,就是要找到mappedStatement被新增的位置。追蹤到AbstractMethod的抽象方法中。
找到了addMappedStatement()方法
而BaseMapper的所有方法(deleteById、delete、insert、select、update等)都繼承了該抽象方法。
根據mapper方法(deleteById)顯然是呼叫addDeleteMappedStatement方法。
這裡我們可以發現,原始碼中根據不同的方法繼承AbstractMethod實現了不同的實現類,並且實現了injectMappedStatement方法,sqlSource也是在這個地方被新增進配置檔案。
4.2 AbstractMethod.inject
繼續研究AbstractMethod抽象類,inject方法實現了自動注入sql的動作。
有上述原始碼可知,專案啟動時,首先由預設注入器生成基礎CRUD實現類物件,其次遍歷實現類列表,依次注入各自的模板SQL,最後將其新增至mappedstatement。
五、SQL語句是怎麼生成的?
5.1 SQL模板
上述方法中有兩個關鍵的引數:SqlMethod、SqlSource;
繼續研究原始碼發現:sqlMethod本質上是一個列舉類,儲存了兩個關鍵的元素:
BaseMapper中的方法名;
方法名對應的sql語句模板(即被
標籤包裹的字串)。
到這裡我們基本瞭解了mybaits-plus實現sql自動生成的本質:根據不同的方法來提供一些通用的模板,專案啟動後再載入進mappedStatement。
5.2 SqlSource
此時SqlSource通過解析SQL模板、以及傳入的表資訊和主鍵資訊構建出了SQL語句。
5.3 資料庫表資訊是如何獲取的?
分析initTableName()方法:獲取表名資訊原始碼中傳入了實體類資訊clazz,其實就是通過實體上的@TableName註解拿到了表名;
我們在定義實體類的同時,指定了該實體類對應的表名。
分析initTableFields()方法:
獲取主鍵及其他欄位資訊
至此tableInfo的資訊已經注入完成了。
在研究完解析mapper的核心過程之後,我們再簡單看下mapper檔案被新增到configuration(mybatis核心配置檔案)的過程。
六、mapper檔案被新增的過程
ISqlInjector:Sql注入器
MybatisMapperAnnotationBuilder:mapper解析器
MybatisMapperAnnotationBuilder中的parse方法獲取了sqlInjector(Sql注入器)來進行SQL隱碼攻擊。
Mybatis新增mapper的固有流程:MybatisMapperRegistry
呼叫MapperAnnotionBuilder解析器進行解析
MybatisConfiguration.addMapper
MybatisXMLConfigBuilder.mapperElemnt
MybayisXMLConfigBuilder.parseConfiguration
新增mapper檔案的過程分析到這裡就完成了。
七、總結
7.1 流程梳理
下面總結梳理了一下mybatis-plus解析mapper檔案自動注入sql的主要流程。
7.2 Mybatis-plus的ORM的核心思想
1)實體類和資料庫表通過自定義註解來完成一一對映。
2)物件屬性和欄位同樣使用註解來一一對應(命名注意要相同)。
3)為了提高複用性使得具體的mapper繼承通用的增刪改查介面。
4)利用模板方法和物件屬性值動態拼接SQL。
八、參考文件
MyBatis-plus官方文件:https://mp.baomidou.com/
作者:vivo網際網路伺服器團隊-Li Lei