前言

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<T> extends Mapper<T> {

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


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

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

测试

@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<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
            if (!mapperRegistryCache.contains(className)) {
            //获取 CRUD 实现类列表
                List<AbstractMethod> 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<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 实现类对象,其次遍历实现类列表,依次注入各自的模板 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);
    }

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

/**
     * <p>
     * 实体类反射获取表信息【初始化】
     * </p>
     *
     * @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<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 注解拿到了表名。

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

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

  /**
     * <p>
     * 初始化 表主键,表字段
     * </p>
     *
     * @param clazz        实体类
     * @param globalConfig 全局配置
     * @param tableInfo    数据库表反射信息
     */
    public static void initTableFields(Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo, List<String> excludeProperty) {
        /* 数据库全局配置 */
        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));

        /* 未发现主键注解,提示警告信息 */
        if (!isReadPK) {
            logger.warn(String.format("Can not find table primary key in Class: \"%s\".", clazz.getName()));
        }
    }

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

到此这篇关于Mybatis-Plus注入SQL原理分析的文章就介绍到这了,更多相关Mybatis-Plus注入SQL内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

Mybatis-Plus注入SQL原理分析的更多相关文章

  1. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  2. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. iOS开发中使用SQL语句操作数据库的基本用法指南

    这篇文章主要介绍了iOS开发中使用SQL语句操作数据库的基本用法指南,包括一些常用SQL语句的整理,需要的朋友可以参考下

  4. 一文搞懂PHP中的DI依赖注入

    依赖注入DI 其实本质上是指对类的依赖通过构造器完成 自动注入。本文将通过一些示例带大家深入了解一下PHP中的DI依赖注入,需要的可以参考一下

  5. MyBatis-Plus乐观锁插件的用法小结

    乐观锁很乐观,对任何事情都保持着一个乐观的态度,认为别人不会修改数据,所以不会上锁,只是在更新数据的时候,去判断这条数据有没有被别人修改过,这篇文章主要介绍了MyBatis-Plus乐观锁插件的用法,需要的朋友可以参考下

  6. Mybatis-Plus主键生成策略的方法

    本文主要介绍了Mybatis-Plus主键生成策略的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  7. mybatis-plus自带QueryWrapper自定义sql实现复杂查询实例详解

    MyBatis-Plus是一个MyBatis(opens new window)的增强工具,在 MyBatis的基础上只做增强不做改变,MyBatis可以无损升级为MyBatis-Plus,这篇文章主要给大家介绍了关于mybatis-plus自带QueryWrapper自定义sql实现复杂查询的相关资料,需要的朋友可以参考下

  8. 使用PHP反射机制来构造"CREATE TABLE"的sql语句

    今天小编就为大家分享一篇关于使用PHP反射机制来构造"CREATE TABLE"的sql语句,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  9. PHP7使用ODBC连接SQL Server2008 R2数据库示例【基于thinkPHP5.1框架】

    这篇文章主要介绍了PHP7使用ODBC连接SQL Server2008 R2数据库,结合实例形式分析了基于thinkPHP5.1框架使用ODBC连接SQL Server2008数据库相关操作技巧,需要的朋友可以参考下

  10. Python创建SQL数据库流程逐步讲解

    会写SQL很重要,能高效地查询数据库被认为是数据分析师/科学家最基本的技能之一。SQL不仅重要,而且非常常用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部