热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

MybatisPlus注入SQL原理分析

Mybatis-Plus注入SQL原理分析-目录前言案例测试原理解析前言MyBatis-Plus是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发

前言

MyBatis-Plus 是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

那么 MyBatis-Plus 是怎么加强的呢?其实就是封装好了一些 crud 方法,开发人员不需要再写 SQL 了,间接调用方法就可以获取到封装好的 SQL 语句。

特性:

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防误操作

支持的数据库:

  • MySQL、 Oracle 、 db2 、PostgreSQL 、 SqlServer 等等。

案例

下面我们先从一个简单的 demo 入手,来感受一下 MyBatis-plus 的便捷性。

MP封装的 BaseMapper 接口

public interface BaseMapper extends Mapper {

    /**
     * 插入一条记录
     *
     * @param entity 实体对象
     */
    int insert(T entity);


    /**
     * 根据 entity 条件,删除记录
     *
     * @param wrapper 实体对象封装操作类(可以为 null)
     */
    int delete(@Param(Constants.WRAPPER) Wrapper wrapper);

    /**
     * 根据 whereEntity 条件,更新记录
     *
     * @param entity        实体对象 (set 条件值,可以为 null)
     * @param updateWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
     */
    int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper updateWrapper);

    /**
     * 根据 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 {
}

测试

@Override
	public User getById(String id){
		User user = userMapper.selectById(id);
		return null;
	}

最终查询的 SQL 语句如下图:

从打印的日志我们可以知道,MyBatis-Plus 最终为我们自动生成了 SQL 语句。根据上述操作分析:UserMapper 继承了 BaseMapper,拥有了 selectById 的方法,但是 MyBatis-Plus 是基于 mybatis 的增强版,关键在于最终仍然需要提供具体的SQL语句,来进行数据库操作。

下面我们 DEBUG 跟踪 MyBatis-Plus 是如何生成业务 sql 以及自动注入的,如下图所示:

发现 SQL 语句在 MappedStatement 对象中,而 sqlSource 存的就是相关的 SQL 语句,基于上面的分析,我们想要知道 SQL 语句是什么时候获取到的,就是要找到 mappedStatement 被添加的位置。追踪到 AbstractMethod 的抽象方法中。

原理解析

Mybatis-Plus 在启动后会将 BaseMapper 中的一系列的方法注册到 meppedStatements 中,那么究竟是如何注入的呢?下面我们一起来分析下。

在 Mybatis-Plus 中,ISqlInjector 负责 SQL 的注入工作,它是一个接口,AbstractSqlInjector 是它的实现类,SqlInjector SQL 自动注入器接口的相关 UML 图如下:

找到了下面我们所讲到的都基于这几个类实现,接着上一个问题,追踪到 AbstractMethod 的抽象方法中,

下面我们继续 DEBUG 跟踪代码是怎么注入的。

首先跳进来 AbstractSqlInjector 抽象类执行 inspectInject 方法

@Override
    public void inspectInject(MapperBuilderAssistant builderAssistant, Class mapperClass) {
        Class modelClass = extractModelClass(mapperClass);
        if (modelClass != null) {
            String className = mapperClass.toString();
            Set mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
            if (!mapperRegistryCache.contains(className)) {
            //获取 CRUD 实现类列表
                List methodList = this.getMethodList(mapperClass);
                if (CollectionUtils.isNotEmpty(methodList)) {
                    TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
                    // 循环注入自定义方法,这里开始注入 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 方法,选择你执行的 CRUD 操作,我这里以 slectById 为例

从这里我们找到了 addMappedStatement() 方法,可以看到,生成了 SqlSource 对象,再将 SQL 通过 addSelectMappedStatement 方法添加到 meppedStatements 中。

那么实现类是怎么获取到的呢?

在 AbstractSqlInjector 抽象类 inspectInject 方法从 this.getMethodList 方法获取,如下图:

这里的 getMethodList 方法获取 CRUD 实现类列表

/**
 * SQL 默认注入器
 *
 * @author hubin
 * @since 2018-04-10
 */
public class DefaultSqlInjector extends AbstractSqlInjector {

    @Override
    public List 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 实现类对象,其次遍历实现类列表,依次注入各自的模板 SQL,最后将其添加至 mappedstatement。

那么 SQL 语句是怎么生成的?此时 SqlSource 通过解析 SQL 模板、以及传入的表信息和主键信息构建出了 SQL 语句,如下所示:

@Override
    public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) {
      /** 定义 mybatis xml method id, 对应  **/
        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);
    }

那么数据库表信息是如何获取的?主要根据AbstractSqlInjector抽象类的 inspectInject 方法中的initTableInfo方法获取,如下图:

/**
     * 

* 实体类反射获取表信息【初始化】 *

* * @param clazz 反射实体类 * @return 数据库表反射信息 */ 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 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 注解拿到了表名。

我们在定义实体类的同时,指定了该实体类对应的表名。

那么获取到表名之后怎么获取主键及其他字段信息呢?主要根据AbstractSqlInjector抽象类的 inspectInject 方法中的initTableFields方法获取,如下图:

/**
     * 

* 初始化 表主键,表字段 *

* * @param clazz 实体类 * @param globalConfig 全局配置 * @param tableInfo 数据库表反射信息 */ public static void initTableFields(Class clazz, GlobalConfig globalConfig, TableInfo tableInfo, List excludeProperty) { /* 数据库全局配置 */ GlobalConfig.DbConfig dbCOnfig= globalConfig.getDbConfig(); ReflectorFactory reflectorFactory = tableInfo.getConfiguration().getReflectorFactory(); //TODO @咩咩 有空一起来撸完这反射模块. Reflector reflector = reflectorFactory.findForClass(clazz); List list = getAllFields(clazz); // 标记是否读取到主键 boolean isReadPK = false; // 是否存在 @TableId 注解 boolean existTableId = isExistTableId(list); List 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)); /* 未发现主键注解,提示警告信息 */ if (!isReadPK) { logger.warn(String.format("Can not find table primary key in Class: \"%s\".", clazz.getName())); } }

到处我们知道 SQL 语句是怎么注入的了,如果想要更加深入了解的小伙伴,可以自己根据上面的源码方法深入去了解。


推荐阅读
  • Python SQLAlchemy库的使用方法详解
    本文详细介绍了Python中使用SQLAlchemy库的方法。首先对SQLAlchemy进行了简介,包括其定义、适用的数据库类型等。然后讨论了SQLAlchemy提供的两种主要使用模式,即SQL表达式语言和ORM。针对不同的需求,给出了选择哪种模式的建议。最后,介绍了连接数据库的方法,包括创建SQLAlchemy引擎和执行SQL语句的接口。 ... [详细]
  • ALTERTABLE通过更改、添加、除去列和约束,或者通过启用或禁用约束和触发器来更改表的定义。语法ALTERTABLEtable{[ALTERCOLUMNcolu ... [详细]
  • PDO MySQL
    PDOMySQL如果文章有成千上万篇,该怎样保存?数据保存有多种方式,比如单机文件、单机数据库(SQLite)、网络数据库(MySQL、MariaDB)等等。根据项目来选择,做We ... [详细]
  • MySQL中的MVVC多版本并发控制机制的应用及实现
    本文介绍了MySQL中MVCC的应用及实现机制。MVCC是一种提高并发性能的技术,通过对事务内读取的内存进行处理,避免写操作堵塞读操作的并发问题。与其他数据库系统的MVCC实现机制不尽相同,MySQL的MVCC是在undolog中实现的。通过undolog可以找回数据的历史版本,提供给用户读取或在回滚时覆盖数据页上的数据。MySQL的大多数事务型存储引擎都实现了MVCC,但各自的实现机制有所不同。 ... [详细]
  • Explain如何助力SQL语句的优化及其分析方法
    本文介绍了Explain如何助力SQL语句的优化以及分析方法。Explain是一个数据库SQL语句的模拟器,通过对SQL语句的模拟返回一个性能分析表,从而帮助工程师了解程序运行缓慢的原因。文章还介绍了Explain运行方法以及如何分析Explain表格中各个字段的含义。MySQL 5.5开始支持Explain功能,但仅限于select语句,而MySQL 5.7逐渐支持对update、delete和insert语句的模拟和分析。 ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • 本文讨论了在数据库打开和关闭状态下,重新命名或移动数据文件和日志文件的情况。针对性能和维护原因,需要将数据库文件移动到不同的磁盘上或重新分配到新的磁盘上的情况,以及在操作系统级别移动或重命名数据文件但未在数据库层进行重命名导致报错的情况。通过三个方面进行讨论。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • 如何在php中将mysql查询结果赋值给变量
    本文介绍了在php中将mysql查询结果赋值给变量的方法,包括从mysql表中查询count(学号)并赋值给一个变量,以及如何将sql中查询单条结果赋值给php页面的一个变量。同时还讨论了php调用mysql查询结果到变量的方法,并提供了示例代码。 ... [详细]
  • MyBatis多表查询与动态SQL使用
    本文介绍了MyBatis多表查询与动态SQL的使用方法,包括一对一查询和一对多查询。同时还介绍了动态SQL的使用,包括if标签、trim标签、where标签、set标签和foreach标签的用法。文章还提供了相关的配置信息和示例代码。 ... [详细]
  • 合并列值-合并为一列问题需求:createtabletab(Aint,Bint,Cint)inserttabselect1,2,3unionallsel ... [详细]
  • 本文介绍了在使用Laravel和sqlsrv连接到SQL Server 2016时,如何在插入查询中使用输出子句,并返回所需的值。同时讨论了使用CreatedOn字段返回最近创建的行的解决方法以及使用Eloquent模型创建后,值正确插入数据库但没有返回uniqueidentifier字段的问题。最后给出了一个示例代码。 ... [详细]
  • MySQL插入数据的四种方式及安全性分析
    本文介绍了MySQL插入数据的四种方式:插入完整的行、插入行的一部分、插入多行和插入查询结果,并对其安全性进行了分析。在插入行时,应注意字段的定义和赋值,以提高安全性。同时指出了使用insert语句的不安全性,应尽量避免使用。建议在表中定义相关字段,并根据定义的字段赋予相应的值,以增加插入操作的安全性。 ... [详细]
author-avatar
王聪明2011
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有