Mybatis-Plus注入SQL原理分析
前言
MyBatis-Plus 是一個 MyBatis 的增強工具,在 MyBatis 的基礎上只做增強不做改變,為簡化開發(fā)、提高效率而生。
那么 MyBatis-Plus 是怎么加強的呢?其實就是封裝好了一些 crud 方法,開發(fā)人員不需要再寫 SQL 了,間接調(diào)用方法就可以獲取到封裝好的 SQL 語句。
特性:
- 無侵入:只做增強不做改變,引入它不會對現(xiàn)有工程產(chǎn)生影響,如絲般順滑
- 損耗?。簡蛹磿詣幼⑷牖?CURD,性能基本無損耗,直接面向?qū)ο蟛僮?/li>
- 強大的 CRUD 操作:內(nèi)置通用 Mapper、通用 Service,僅僅通過少量配置即可實現(xiàn)單表大部分 CRUD 操作,更有強大的條件構造器,滿足各類使用需求
- 支持 Lambda 形式調(diào)用:通過 Lambda 表達式,方便的編寫各類查詢條件,無需再擔心字段寫錯
- 支持主鍵自動生成:支持多達 4 種主鍵策略(內(nèi)含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解決主鍵問題
- 支持 ActiveRecord 模式:支持 ActiveRecord 形式調(diào)用,實體類只需繼承 Model 類即可進行強大的 CRUD 操作
- 支持自定義全局通用操作:支持全局通用方法注入( Write once, use anywhere )
- 內(nèi)置代碼生成器:采用代碼或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 層代碼,支持模板引擎,更有超多自定義配置等您來使用
- 內(nèi)置分頁插件:基于 MyBatis 物理分頁,開發(fā)者無需關心具體操作,配置好插件之后,寫分頁等同于普通 List 查詢
- 分頁插件支持多種數(shù)據(jù)庫:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多種數(shù)據(jù)庫
- 內(nèi)置性能分析插件:可輸出 Sql 語句以及其執(zhí)行時間,建議開發(fā)測試時啟用該功能,能快速揪出慢查詢
- 內(nèi)置全局攔截插件:提供全表 delete 、 update 操作智能分析阻斷,也可自定義攔截規(guī)則,預防誤操作
支持的數(shù)據(jù)庫:
- MySQL、 Oracle 、 db2 、PostgreSQL 、 SqlServer 等等。
案例
下面我們先從一個簡單的 demo 入手,來感受一下 MyBatis-plus 的便捷性。
MP封裝的 BaseMapper 接口
public interface BaseMapper<T> extends Mapper<T> { /** * 插入一條記錄 * * @param entity 實體對象 */ int insert(T entity); /** * 根據(jù) entity 條件,刪除記錄 * * @param wrapper 實體對象封裝操作類(可以為 null) */ int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper); /** * 根據(jù) whereEntity 條件,更新記錄 * * @param entity 實體對象 (set 條件值,可以為 null) * @param updateWrapper 實體對象封裝操作類(可以為 null,里面的 entity 用于生成 where 語句) */ int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper); /** * 根據(jù) ID 查詢 * * @param id 主鍵ID */ T selectById(Serializable id); }
實體類對象
/** * 實體類 * * @author Chill */ @Data @TableName("user") @EqualsAndHashCode(callSuper = true) public class User extends TenantEntity { private static final long serialVersionUID = 1L; /** * 用戶編號 */ private String code; /** * 賬號 */ private String account; /** * 密碼 */ private String password; /** * 昵稱 */ private String name; }
UserMapper 繼承 BaseMapper 接口
/** * Mapper 接口 * * @author Chill */ public interface UserMapper extends BaseMapper<User> { }
測試
@Override public User getById(String id){ User user = userMapper.selectById(id); return null; }
最終查詢的 SQL 語句如下圖:
從打印的日志我們可以知道,MyBatis-Plus 最終為我們自動生成了 SQL 語句。根據(jù)上述操作分析:UserMapper 繼承了 BaseMapper,擁有了 selectById 的方法,但是 MyBatis-Plus 是基于 mybatis 的增強版,關鍵在于最終仍然需要提供具體的SQL語句,來進行數(shù)據(jù)庫操作。
下面我們 DEBUG 跟蹤 MyBatis-Plus 是如何生成業(yè)務 sql 以及自動注入的,如下圖所示:
發(fā)現(xiàn) SQL 語句在 MappedStatement 對象中,而 sqlSource 存的就是相關的 SQL 語句,基于上面的分析,我們想要知道 SQL 語句是什么時候獲取到的,就是要找到 mappedStatement 被添加的位置。追蹤到 AbstractMethod 的抽象方法中。
原理解析
Mybatis-Plus 在啟動后會將 BaseMapper 中的一系列的方法注冊到 meppedStatements 中,那么究竟是如何注入的呢?下面我們一起來分析下。
在 Mybatis-Plus 中,ISqlInjector 負責 SQL 的注入工作,它是一個接口,AbstractSqlInjector 是它的實現(xiàn)類,SqlInjector SQL 自動注入器接口的相關 UML 圖如下:
找到了下面我們所講到的都基于這幾個類實現(xiàn),接著上一個問題,追蹤到 AbstractMethod 的抽象方法中,
下面我們繼續(xù) DEBUG 跟蹤代碼是怎么注入的。
首先跳進來 AbstractSqlInjector 抽象類執(zhí)行 inspectInject 方法
@Override public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) { Class<?> modelClass = extractModelClass(mapperClass); if (modelClass != null) { String className = mapperClass.toString(); Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration()); if (!mapperRegistryCache.contains(className)) { //獲取 CRUD 實現(xiàn)類列表 List<AbstractMethod> methodList = this.getMethodList(mapperClass); if (CollectionUtils.isNotEmpty(methodList)) { TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass); // 循環(huán)注入自定義方法,這里開始注入 sql methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo)); } else { logger.debug(mapperClass.toString() + ", No effective injection method was found."); } mapperRegistryCache.add(className); } } }
在這里我們找到 inject 方法,跳進去
在跳進去 injectMappedStatement 方法,選擇你執(zhí)行的 CRUD 操作,我這里以 slectById 為例
從這里我們找到了 addMappedStatement() 方法,可以看到,生成了 SqlSource 對象,再將 SQL 通過 addSelectMappedStatement 方法添加到 meppedStatements 中。
那么實現(xiàn)類是怎么獲取到的呢?
在 AbstractSqlInjector 抽象類 inspectInject 方法從 this.getMethodList 方法獲取,如下圖:
這里的 getMethodList 方法獲取 CRUD 實現(xiàn)類列表
/** * SQL 默認注入器 * * @author hubin * @since 2018-04-10 */ public class DefaultSqlInjector extends AbstractSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass) { return Stream.of( new Insert(), new Delete(), new DeleteByMap(), new DeleteById(), new DeleteBatchByIds(), new Update(), new UpdateById(), new SelectById(), new SelectBatchByIds(), new SelectByMap(), new SelectOne(), new SelectCount(), new SelectMaps(), new SelectMapsPage(), new SelectObjs(), new SelectList(), new SelectPage() ).collect(toList()); } }
從上面的源碼可知,項目啟動時,首先由默認注入器生成基礎 CRUD 實現(xiàn)類對象,其次遍歷實現(xiàn)類列表,依次注入各自的模板 SQL,最后將其添加至 mappedstatement。
那么 SQL 語句是怎么生成的?此時 SqlSource 通過解析 SQL 模板、以及傳入的表信息和主鍵信息構建出了 SQL 語句,如下所示:
@Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { /** 定義 mybatis xml method id, 對應 <id="xyz"> **/ SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID; /** 構造 id 對應的具體 xml 片段 **/ SqlSource sqlSource = new RawSqlSource(configuration, String.format(sqlMethod.getSql(), sqlSelectColumns(tableInfo, false), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(), tableInfo.getLogicDeleteSql(true, true)), Object.class); /** 將 xml method 方法添加到 mybatis 的 MappedStatement 中 **/ return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo); }
那么數(shù)據(jù)庫表信息是如何獲取的?主要根據(jù)AbstractSqlInjector抽象類的 inspectInject 方法中的initTableInfo方法獲取,如下圖:
/** * <p> * 實體類反射獲取表信息【初始化】 * </p> * * @param clazz 反射實體類 * @return 數(shù)據(jù)庫表反射信息 */ public synchronized static TableInfo initTableInfo(MapperBuilderAssistant builderAssistant, Class<?> clazz) { TableInfo tableInfo = TABLE_INFO_CACHE.get(clazz); if (tableInfo != null) { if (builderAssistant != null) { tableInfo.setConfiguration(builderAssistant.getConfiguration()); } return tableInfo; } /* 沒有獲取到緩存信息,則初始化 */ tableInfo = new TableInfo(clazz); GlobalConfig globalConfig; if (null != builderAssistant) { tableInfo.setCurrentNamespace(builderAssistant.getCurrentNamespace()); tableInfo.setConfiguration(builderAssistant.getConfiguration()); globalConfig = GlobalConfigUtils.getGlobalConfig(builderAssistant.getConfiguration()); } else { // 兼容測試場景 globalConfig = GlobalConfigUtils.defaults(); } /* 初始化表名相關 */ final String[] excludeProperty = initTableName(clazz, globalConfig, tableInfo); List<String> excludePropertyList = excludeProperty != null && excludeProperty.length > 0 ? Arrays.asList(excludeProperty) : Collections.emptyList(); /* 初始化字段相關 */ initTableFields(clazz, globalConfig, tableInfo, excludePropertyList); /* 放入緩存 */ TABLE_INFO_CACHE.put(clazz, tableInfo); /* 緩存 lambda */ LambdaUtils.installCache(tableInfo); /* 自動構建 resultMap */ tableInfo.initResultMapIfNeed(); return tableInfo; }
分析 initTableName() 方法,獲取表名信息源碼中傳入了實體類信息 class,其實就是通過實體上的@TableName 注解拿到了表名。
我們在定義實體類的同時,指定了該實體類對應的表名。
那么獲取到表名之后怎么獲取主鍵及其他字段信息呢?主要根據(jù)AbstractSqlInjector抽象類的 inspectInject 方法中的initTableFields方法獲取,如下圖:
/** * <p> * 初始化 表主鍵,表字段 * </p> * * @param clazz 實體類 * @param globalConfig 全局配置 * @param tableInfo 數(shù)據(jù)庫表反射信息 */ public static void initTableFields(Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo, List<String> excludeProperty) { /* 數(shù)據(jù)庫全局配置 */ GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig(); ReflectorFactory reflectorFactory = tableInfo.getConfiguration().getReflectorFactory(); //TODO @咩咩 有空一起來擼完這反射模塊. Reflector reflector = reflectorFactory.findForClass(clazz); List<Field> list = getAllFields(clazz); // 標記是否讀取到主鍵 boolean isReadPK = false; // 是否存在 @TableId 注解 boolean existTableId = isExistTableId(list); List<TableFieldInfo> fieldList = new ArrayList<>(list.size()); for (Field field : list) { if (excludeProperty.contains(field.getName())) { continue; } /* 主鍵ID 初始化 */ if (existTableId) { TableId tableId = field.getAnnotation(TableId.class); if (tableId != null) { if (isReadPK) { throw ExceptionUtils.mpe("@TableId can't more than one in Class: \"%s\".", clazz.getName()); } else { isReadPK = initTableIdWithAnnotation(dbConfig, tableInfo, field, tableId, reflector); continue; } } } else if (!isReadPK) { isReadPK = initTableIdWithoutAnnotation(dbConfig, tableInfo, field, reflector); if (isReadPK) { continue; } } /* 有 @TableField 注解的字段初始化 */ if (initTableFieldWithAnnotation(dbConfig, tableInfo, fieldList, field)) { continue; } /* 無 @TableField 注解的字段初始化 */ fieldList.add(new TableFieldInfo(dbConfig, tableInfo, field)); } /* 檢查邏輯刪除字段只能有最多一個 */ Assert.isTrue(fieldList.parallelStream().filter(TableFieldInfo::isLogicDelete).count() < 2L, String.format("@TableLogic can't more than one in Class: \"%s\".", clazz.getName())); /* 字段列表,不可變集合 */ tableInfo.setFieldList(Collections.unmodifiableList(fieldList)); /* 未發(fā)現(xiàn)主鍵注解,提示警告信息 */ if (!isReadPK) { logger.warn(String.format("Can not find table primary key in Class: \"%s\".", clazz.getName())); } }
到處我們知道 SQL 語句是怎么注入的了,如果想要更加深入了解的小伙伴,可以自己根據(jù)上面的源碼方法深入去了解。
到此這篇關于Mybatis-Plus注入SQL原理分析的文章就介紹到這了,更多相關Mybatis-Plus注入SQL內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JAVA 筆記 ClassLoader.getResourceAsStream() 與 Class.getResourc
這篇文章主要介紹了JAVA 筆記 ClassLoader.getResourceAsStream() 與 Class.getResourceAsStream()的區(qū)別,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-07-07解決eclipse啟動tomcat時不能加載web項目的問題
這篇文章主要介紹了解決eclipse啟動tomcat時不能加載web項目的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06SpringBoot2.x中management.security.enabled=false無效的解決
這篇文章主要介紹了SpringBoot2.x中management.security.enabled=false無效的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07