Mybatis Plus详解【一】

一、简介

MybatisPlus可以节省大量时间,所有的CRUD代码都可以自动化完成。MyBatis-Plus是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

特性:

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

二、快速入门

:本文实验环境为Spring Boot + MySQL。

1. 加入依赖

<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>3.1.6version>
    <relativePath/> 
parent>
<dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>com.mysqlgroupId>
            <artifactId>mysql-connector-jartifactId>
        dependency>
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.5.3.1version>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.4version>
        dependency>
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.12version>
        dependency>
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-log4j12artifactId>
            <version>1.6.4version>
        dependency>
        <dependency>
            <groupId>org.testnggroupId>
            <artifactId>testngartifactId>
            <version>RELEASEversion>
            <scope>testscope>
        dependency>
    dependencies>

2, 创建数据库表

DROP TABLE IF EXISTS user;

CREATE TABLE user(
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT(11) NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
);

DELETE FROM user;

INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, '[email protected]'),
(2, 'Jack', 20, '[email protected]'),
(3, 'Tom', 28, '[email protected]'),
(4, 'Sandy', 21, '[email protected]'),
(5, 'Billie', 24, '[email protected]');

3. 编写application.yml

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis_plus?userUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

4. 引入log4j.properties

log4j.rootLogger=DEBUG,A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=[%t] [%c]-[%p] %m%n

5. 编写pojo

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

6. 编写mapper

@Mapper
public interface UserMapper extends BaseMapper<User> {
//所有的CRUD都已经完成,不需要像以前一样配置一大堆文件:pojo->dao(连接mybatis,配置mapper.xml文件)->service->controller
}

7. 编写启动类

@SpringBootApplication
@MapperScan("com.shiftycat.mybatis_plus.mapper")
public class MybatisPlusApplication {
    public static void main(String[] args) {
        SpringApplication.run(MybatisPlusApplication.class, args);
    }

}

8. 测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTest {
    @Autowired
    private UserMapper userMapper;

    @Test
    public void testSelect() {
        List<User> userList = userMapper.selectList(null);
        for (User user : userList) {
            System.out.println(user);
        }
    }
}

三、Mapper层的通用CRUD

1. 插入操作

// int insert(T entity);
@Test
public void testInsert() {
    User user = new User();
    user.setAge(18);
    user.setEmail("[email protected]");
    user.setName("张三");

    //返回的result是受影响的⾏数,并不是⾃增后的id
    int result = userMapper.insert(user);
    System.out.println(result);
    System.out.println(user.getId());
}
1)@TableId

通过数据库可以看到,数据已经写入到了数据库,但是,id的值不正确,我们期望的是数据库自增长,实际是MP生成了id的值写入到了数据库。

解决方案:自定义ID生成器

在复杂分布式系统中,往往需要大量的数据和消息进行唯一标识。比如支付宝每一个账号在数据库分表后都需要有一个唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。所以,生成的ID需要具备一下特点:

  1. 全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
  2. 趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
  3. 单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
  4. 信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO) //指定id类型为自增长
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

通过@TableId(type = IdType.AUTO)来设置主键ID自增长,ctrl+鼠标左键可查看剩下的type枚举类型。主要主键生成方式有:

  • 数据库ID自增(AUTO)

  • 雪花算法(ASSIGN_ID)

    这种方案是一种以划分命名空间来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等。使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生4096个ID),最后还有一个符号位,永远是0。

    优点

    • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
    • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
    • 可以根据自身业务特性分配bit位,非常灵活。

    缺点

    • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
  • UUID(ASSIGN_UUID)

    UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符。

    优点

    • 性能非常高:本地生成,没有网络消耗。

    缺点

    • 没有排序,无法保证趋势递增。
    • UUID往往使用字符串存储,查询的效率比较低。
    • 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
    • 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
    • ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用:
      • MySQL官方有明确的建议主键要尽量越短越好[4],36个字符长度的UUID不符合要求。
      • 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。

:自 3.3.0 开始,默认使用雪花算法+UUID(不含中划线)

public enum IdType {
	// 数据库ID⾃增
    AUTO(0),
    // 该类型为未设置主键类型
    NONE(1),
    // ⽤户输⼊ID,该类型可以通过⾃⼰注册⾃动填充插件进⾏填充
    INPUT(2),
    // 分配ID (主键类型为number或string)(雪花算法)
    ASSIGN_ID(3),
    // 分配UUID (主键类型为 string)
    ASSIGN_UUID(4);

    private final int key;

    private IdType(int key) {
        this.key = key;
    }

    public int getKey() {
        return this.key;
    }
}
2)@TableField

@TableField注解可以指定字段的⼀些属性:

  1. value:指定属性对应的数据库表字段名。如果属性名与表字段名一致,可以不用设置该属性。
  2. exist:设置属性是否为数据库表字段,默认为true。如果设置为false,则表示该属性不对应任何数据库字段。
  3. select:设置查询时是否查询该属性,默认为true。如果设置为false,则表示查询时不查询该属性。
  4. insert:设置插入时是否插入该属性,默认为true。如果设置为false,则表示插入时不插入该属性。
  5. update:设置更新时是否更新该属性,默认为true。如果设置为false,则表示更新时不更新该属性。
  6. keepGlobalFormat:设置是否保持全局的命名规则,默认为false。如果设置为true,则属性名将按照全局的命名规则进行转换。
  7. condition:设置查询条件,只有满足条件的数据才会查询到该属性。

几个常见的应用场景:

  1. 映射数据库表字段名与实体类属性名不一致

    通过设置value属性,可以灵活地指定属性对应的数据库表字段名,避免在查询和更新操作时发生字段名不匹配的错误。

  2. 设置插入和更新时需要忽略的字段

    通过设置insertupdate属性,可以灵活地控制是否插入或者更新某个属性,避免不必要的数据库操作。

  3. 根据条件查询指定的字段

    通过设置condition属性,可以指定查询时的条件,只有满足条件的数据才会查询到该字段,提高查询性能。

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO) //指定id类型为自增长
    private Long id;
    private String name;
    @TableField(select = false)
    private Integer age;
    @TableField(value = "email")
    private String mail;
    @TableField(exist = false)
    private String address;
}
3)@TableName

@TableName用来设置实体类对应的表明。如果我们不设置这个注解,我们操作的数据库的表就由BaseMapper 泛型决定(User)

  • value作用:value指定数据库中的表名
@TableName(value="user")
public class User {
    ......
}

另外的一种方法是在ymal文件中设置实体类所对应的表的统一前缀。

mybatis-plus:
  global-config:
    db-config:
      table-prefix: db_

2. 更新操作

1)根据主键更新
// 据 ID 修改
// int updateById(@Param("et") T entity);
@Test
public void testUpdateById() {
    User user = new User();
    user.setId(5L); //主键
    user.setAge(21); //更新的字段
    //根据id更新,更新不为null的字段
    this.userMapper.updateById(user);
}
2)根据条件更新
/**
 * 根据 whereEntity 条件,更新记录
 * 正常的更新sql语句为:update 表名 set 字段 = 值 where 条件语句;
 * @param entity 实体对象 (set 条件值,可以为 null)
 * @param updateWrapper 实体对象封装操作类(可以为 null,⾥⾯的 entity ⽤于⽣成 where 语句)
 */
// int update(@Param("et") T entity, @Param("ew") Wrapper updateWrapper);
@Test
public void testUpdate1() {
    /*
     根据条件进行更新:将姓名为“张三”的年龄修改为30岁。
    */
    User user = new User();
    //更新的字段
    user.setAge(30);
    //更新的条件
    QueryWrapper<User> wrapper = new QueryWrapper<>();
    wrapper.eq("name", "张三");
    //执⾏更新操作
    int result = this.userMapper.update(user, wrapper);
    System.out.println("result = " + result);
}

@Test
public void testUpdate2() {
    // 方法2:通过UpdateWrapper进⾏更新
    //更新的条件以及字段
    UpdateWrapper<User> wrapper=new UpdateWrapper<>();
    wrapper.eq("name","张三").set("age",28);
    //执⾏更新操作
    int result=this.userMapper.update(null,wrapper);
    System.out.println("result = "+result);
}

3. 删除操作

1)根据主键删除
// int deleteById(T entity);
@Test
public void testDeleteById() {
    //执⾏删除操作
    int result = this.userMapper.deleteById(5L);
    System.out.println("result = " + result);
}
2)根据条件删除
// int deleteByMap(@Param("cm") Map columnMap);
@Test
public void testDeleteByMap1() {
    Map<String, Object> columnMap = new HashMap<>();
    columnMap.put("age", 28);
    columnMap.put("name", "张三");
    //将columnMap中的元素设置为删除的条件,多个之间为and关系
    int result = this.userMapper.deleteByMap(columnMap);
    System.out.println("result = " + result);
}

// int delete(@Param("ew") Wrapper queryWrapper);
@Test
public void testDeleteByMap2() {
    User user = new User();
    user.setAge(18);
    user.setName("张三");
    //将实体对象进⾏包装,包装为操作条件
    QueryWrapper<User> wrapper = new QueryWrapper<>(user);
    int result = this.userMapper.delete(wrapper);
    System.out.println("result = " + result);
}
3)根据主键批量删除
// int deleteBatchIds(@Param("coll") Collection idList);
@Test
public void testDeleteByMap() {
    //根据id集合批量删除
    int result = this.userMapper.deleteBatchIds(Arrays.asList(2L, 3L));
    System.out.println("result = " + result);
}

4. 查询操作

1)根据主键查询
// T selectById(Serializable id);
@Test
public void testSelectById() {
    //根据id查询数据
    User user = this.userMapper.selectById(1L);
    System.out.println("result = " + user);
}
2)根据主键批量查询
// List selectBatchIds(@Param("coll") Collection idList);
@Test
public void testSelectBatchIds() {
    //根据id集合批量查询
    List<User> users = this.userMapper.selectBatchIds(Arrays.asList(1L, 4L));
    for (User user : users) {
        System.out.println(user);
    }
}
3)根据条件查询单个记录

根据 entity 条件,查询⼀条记录,如果查询结果超过一条会报错。

default T selectOne(@Param("ew") Wrapper<T> queryWrapper) {
    List<T> list = this.selectList(queryWrapper);
    if (list.size() == 1) {
        return list.get(0);
    } else if (list.size() > 1) {
        throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
        return null;
    }
}
@Test
public void testSelectOne() {
    QueryWrapper<User> wrapper = new QueryWrapper<User>();
    wrapper.eq("name", "Jone");
    //根据条件查询⼀条数据,如果结果超过⼀条会报错
    User user = this.userMapper.selectOne(wrapper);
    System.out.println(user);
}
4)根据条件查询总记录数
// Long selectCount(@Param("ew") Wrapper queryWrapper);
@Test
public void testSelectCount() {
    QueryWrapper<User> wrapper = new QueryWrapper<User>();
    wrapper.gt("age", 20); //年龄⼤于20岁
    //根据条件查询数据条数
    Long count = this.userMapper.selectCount(wrapper);
    System.out.println("count = " + count);
}
5)根据条件查询全部记录
// List selectList(@Param("ew") Wrapper queryWrapper);
@Test
public void testSelectList1() {
    QueryWrapper<User> wrapper = new QueryWrapper<User>();
    wrapper.gt("age", 23); //年龄⼤于23岁
    //根据条件查询数据
    List<User> users = this.userMapper.selectList(wrapper);
    for (User user : users) {
        System.out.println("user = " + user);
    }
}

// List> selectMaps(@Param("ew") Wrapper queryWrapper);
// 方法返回List>类型的值
@Test
public void testSelectList2() {
    QueryWrapper<User> wrapper = new QueryWrapper<User>();
    wrapper.gt("age", 23); //年龄⼤于23岁
    //根据条件查询数据
    List<Map<String, Object>> users = this.userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}
// List selectObjs(@Param("ew") Wrapper queryWrapper);
// 只返回第一个字段的值
@Test
public void testSelectList3() {
    QueryWrapper<User> wrapper = new QueryWrapper<User>();
    wrapper.gt("age", 23); //年龄⼤于23岁
    //根据条件查询数据
    List<Object> users = this.userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}
 
  
6)根据条件查询全部记录并分页

配置分页插件

@Configuration
@MapperScan("com.shiftycat.mybatis_plus.mapper") //设置mapper接⼝的扫描包
public class MybatisPlusConfig {
    /**
     * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
     */

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
// 

> P selectPage(P page, @Param("ew") Wrapper queryWrapper); @Test public void testSelectPage() { QueryWrapper<User> wrapper = new QueryWrapper<User>(); wrapper.gt("age", 10); //年龄⼤于10岁 //第一个参数:当前页 第二个参数:每页的条数 Page<User> page = new Page<>(2, 1); //根据条件查询数据 IPage<User> iPage = this.userMapper.selectPage(page, wrapper); System.out.println("数据总条数:" + iPage.getTotal()); System.out.println("总⻚数:" + iPage.getPages()); System.out.println("分页数据:" + iPage.getRecords()); }

四、SQL自动注入原理

1. MappedStatement 对象

在 Mybatis Plus 中,ISqlInjector 接口负责 SQL 的注入工作,AbstractSqlInjector 是它的实现类。

  • 首先,AbstractSqlInjector 抽象类执行 inspectInject 方法。在该方法中,使用this.getMethodList()获取到实现类的列表,并且对于列表中的每一个元素运行inject()方法。

    public abstract class AbstractSqlInjector implements ISqlInjector {
        protected final Log logger = LogFactory.getLog(this.getClass());
    
        public AbstractSqlInjector() {
        }
        // inspectInject()方法
        public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
            Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);
            if (modelClass != null) {
                String className = mapperClass.toString();
                Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
                if (!mapperRegistryCache.contains(className)) {
                    TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
                    // 获取 CRUD 实现类列表
                    List<AbstractMethod> methodList = this.getMethodList(mapperClass, tableInfo);
                    if (CollectionUtils.isNotEmpty(methodList)) {
                        methodList.forEach((m) -> {
                            // inject()方法在这边
                            m.inject()(builderAssistant, mapperClass, modelClass, tableInfo);
                        });
                    } else {
                        this.logger.debug(mapperClass.toString() + ", No effective injection method was found.");
                    }
                    mapperRegistryCache.add(className);
                }
            }
        }
        
        public abstract List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo);
    }
    
  • 其次,进入 inject() 方法。

    public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
            this.configuration = builderAssistant.getConfiguration();
            this.builderAssistant = builderAssistant;
            this.languageDriver = this.configuration.getDefaultScriptingLanguageInstance();
        	// injectMappedStatement()方法在这边
            this.injectMappedStatement(mapperClass, modelClass, tableInfo);
        }
    
  • 再进入 injectMappedStatement() 方法,ctrl+Alt选择你执行的 CRUD 操作。以 SelectById 为例,生成了 SqlSource 对象,再将 SQL 通过 addSelectMappedStatementForTable() 方法添加到 meppedStatements 中。

    public class SelectById extends AbstractMethod {
        public SelectById() {
            this(SqlMethod.SELECT_BY_ID.getMethod());
        }
    
        public SelectById(String name) {
            super(name);
        }
    
        public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
            SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
            SqlSource sqlSource = new RawSqlSource(this.configuration, String.format(sqlMethod.getSql(), this.sqlSelectColumns(tableInfo, false), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(), tableInfo.getLogicDeleteSql(true, true)), Object.class);
            // addSelectMappedStatementForTable()方法在这边
            return this.addSelectMappedStatementForTable(mapperClass, this.methodName, sqlSource, tableInfo);
        }
    }
    
  • 实现类是如何获取到 meppedStatements 返回值:在 AbstractSqlInjector 抽象类 inspectInject 方法从 this.getMethodList() 方法获取。

    public abstract class AbstractSqlInjector implements ISqlInjector {
        protected final Log logger = LogFactory.getLog(this.getClass());
    
        public AbstractSqlInjector() {
        }
    
        public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
            ......
            // 获取 CRUD 实现类列表
            List<AbstractMethod> methodList = this.getMethodList(mapperClass, tableInfo);
            ......
        }
    	// getMethodList()方法
        public abstract List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo);
    }
    
  • DefaultSqlInjector 中的 getMethodList() 方法获取CRUD实现类列表。

    public class DefaultSqlInjector extends AbstractSqlInjector {
        public DefaultSqlInjector() {
        }
    
        public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
            Stream.Builder<AbstractMethod> builder = Stream.builder()
                .add(new Insert())
                .add(new Delete())
                .add(new DeleteByMap())
                .add(new Update())
                .add(new SelectByMap())
                .add(new SelectCount())
                .add(new SelectMaps())
                .add(new SelectMapsPage())
                .add(new SelectObjs())
                .add(new SelectList())
                .add(new SelectPage());
            if (tableInfo.havePK()) {
                builder
                    .add(new DeleteById())
                    .add(new DeleteBatchByIds())
                    .add(new UpdateById())
                    .add(new SelectById())
                    .add(new SelectBatchByIds());
            } else {
                this.logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.", tableInfo.getEntityType()));
            }
            return (List)builder.build().collect(Collectors.toList());
        }
    }
    

总结:项目启动时,首先由默认注入器DefaultSqlInjector生成基础 CRUD 实现类对象,其次遍历实现类列表,依次注入各自的模板 SQL,最后将其添加至 mappedstatement

2. SQL 语句生成

SqlSource 通过解析SQL 模板、以及传入的表信息主键信息构建出了 SQL 语句。

  • injectMappedStatement() 方法中,首先获取在mapper.xml method id

    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(this.configuration, String.format(sqlMethod.getSql(), this.sqlSelectColumns(tableInfo, false), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(), tableInfo.getLogicDeleteSql(true, true)), Object.class);
        // 将 xml method 方法添加到 mybatis 的 MappedStatement 中
        return this.addSelectMappedStatementForTable(mapperClass, this.methodName, sqlSource, tableInfo);
    }
    
  • 根据 AbstractSqlInjector 抽象类的 inspectInject 方法中的 initTableInfo 方法获取数据库表信息。

    public static synchronized TableInfo initTableInfo(MapperBuilderAssistant builderAssistant, Class<?> clazz) {
        // 获取数据库表缓存信息
        TableInfo targetTableInfo = (TableInfo)TABLE_INFO_CACHE.get(clazz);
        Configuration configuration = builderAssistant.getConfiguration();
        // 获取到缓存信息
        if (targetTableInfo != null) {
            Configuration oldConfiguration = targetTableInfo.getConfiguration();
            // 配置信息变化,则初始化
            if (!oldConfiguration.equals(configuration)) {
                targetTableInfo = initTableInfo(configuration, builderAssistant.getCurrentNamespace(), clazz);
            }
            
            return targetTableInfo;
        } else {
            // 没有获取到缓存信息,则初始化
            return initTableInfo(configuration, builderAssistant.getCurrentNamespace(), clazz);
        }
    }
    
    private static synchronized TableInfo initTableInfo(Configuration configuration, String currentNamespace, Class<?> clazz) {
        TableInfo tableInfo = new TableInfo(configuration, clazz);
        tableInfo.setCurrentNamespace(currentNamespace);
        GlobalConfig globalConfig = GlobalConfigUtils.getGlobalConfig(configuration);
        // 初始化表名相关
        String[] excludeProperty = initTableName(clazz, globalConfig, tableInfo);
        List<String> excludePropertyList = excludeProperty != null && excludeProperty.length > 0 ? Arrays.asList(excludeProperty) : Collections.emptyList();
        // 初始化字段相关
        initTableFields(configuration, clazz, globalConfig, tableInfo, excludePropertyList);
        // 自动构建 resultMap
        tableInfo.initResultMapIfNeed();
        globalConfig.getPostInitTableInfoHandler().postTableInfo(tableInfo, configuration);
        // 放入缓存
        TABLE_INFO_CACHE.put(clazz, tableInfo);
        TABLE_NAME_INFO_CACHE.put(tableInfo.getTableName(), tableInfo);
        // 缓存 lambda
        LambdaUtils.installCache(tableInfo);
        return tableInfo;
    }
    
  • initTableName() 方法,获取表名信息源码中传入了实体类信息 class,其实就是通过实体上的@TableName 注解拿到了表名。

    private static String[] initTableName(Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo) {
        GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
        // 通过实体上的@TableName 注解拿到了表名
        TableName table = (TableName)clazz.getAnnotation(TableName.class);
        String tableName = clazz.getSimpleName();
        String tablePrefix = dbConfig.getTablePrefix();
        String schema = dbConfig.getSchema();
        boolean tablePrefixEffect = true;
        String[] excludeProperty = null;
        if (table != null) {
            if (StringUtils.isNotBlank(table.value())) {
                tableName = table.value();
                if (StringUtils.isNotBlank(tablePrefix) && !table.keepGlobalPrefix()) {
                    tablePrefixEffect = false;
                }
            } else {
                tableName = initTableNameWithDbConfig(tableName, dbConfig);
            }
    
            if (StringUtils.isNotBlank(table.schema())) {
                schema = table.schema();
            }
    
            if (StringUtils.isNotBlank(table.resultMap())) {
                tableInfo.setResultMap(table.resultMap());
            }
    
            tableInfo.setAutoInitResultMap(table.autoResultMap());
            excludeProperty = table.excludeProperty();
        } else {
            tableName = initTableNameWithDbConfig(tableName, dbConfig);
        }
    
        String targetTableName = tableName;
        if (StringUtils.isNotBlank(tablePrefix) && tablePrefixEffect) {
            targetTableName = tablePrefix + tableName;
        }
    
        if (StringUtils.isNotBlank(schema)) {
            targetTableName = schema + "." + targetTableName;
        }
    
        tableInfo.setTableName(targetTableName);
        if (CollectionUtils.isNotEmpty(dbConfig.getKeyGenerators())) {
            tableInfo.setKeySequence((KeySequence)clazz.getAnnotation(KeySequence.class));
        }
    
        return excludeProperty;
    }
    
  • 根据 AbstractSqlInjector 抽象类的 inspectInject 方法中的 initTableFields 方法获取主键及其他字段信息。

    private static void initTableFields(Configuration configuration, Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo, List<String> excludeProperty) {
        // 数据库全局配置
        GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
        PostInitTableInfoHandler postInitTableInfoHandler = globalConfig.getPostInitTableInfoHandler();
        Reflector reflector = tableInfo.getReflector();
        List<Field> list = getAllFields(clazz);
        // 标记是否读取到主键
        boolean isReadPK = false;
        // 是否存在 @TableId 注解
        boolean existTableId = isExistTableId(list);
        boolean existTableLogic = isExistTableLogic(list);
        List<TableFieldInfo> fieldList = new ArrayList(list.size());
        Iterator var13 = list.iterator();
        
        while(var13.hasNext()) {
            Field field = (Field)var13.next();
            if (!excludeProperty.contains(field.getName())) {
                boolean isPK = false;
                boolean isOrderBy = field.getAnnotation(OrderBy.class) != null;
                if (existTableId) {
                    // 主键ID初始化
                    TableId tableId = (TableId)field.getAnnotation(TableId.class);
                    if (tableId != null) {
                        if (isReadPK) {
                            throw ExceptionUtils.mpe("@TableId can't more than one in Class: \"%s\".", new Object[]{clazz.getName()});
                        }
    
                        initTableIdWithAnnotation(dbConfig, tableInfo, field, tableId);
                        isReadPK = true;
                        isPK = true;
                    }
                } else if (!isReadPK) {
                    isPK = isReadPK = initTableIdWithoutAnnotation(dbConfig, tableInfo, field);
                }
    
                if (isPK) {
                    if (isOrderBy) {
                        tableInfo.getOrderByFields().add(new TableFieldInfo(dbConfig, tableInfo, field, reflector, existTableLogic, true));
                    }
                } else {
                    TableField tableField = (TableField)field.getAnnotation(TableField.class);
                    TableFieldInfo tableFieldInfo;
                    if (tableField != null) {
                        tableFieldInfo = new TableFieldInfo(dbConfig, tableInfo, field, tableField, reflector, existTableLogic, isOrderBy);
                        fieldList.add(tableFieldInfo);
                        postInitTableInfoHandler.postFieldInfo(tableFieldInfo, configuration);
                    } else {
                        tableFieldInfo = new TableFieldInfo(dbConfig, tableInfo, field, reflector, existTableLogic, isOrderBy);
                        fieldList.add(tableFieldInfo);
                        postInitTableInfoHandler.postFieldInfo(tableFieldInfo, configuration);
                    }
                }
            }
        }
    
        tableInfo.setFieldList(fieldList);
        // 未发现主键注解,提示警告信息
        if (!isReadPK) {
            logger.warn(String.format("Can not find table primary key in Class: \"%s\".", clazz.getName()));
        }
    
    }
    

你可能感兴趣的:(Mybatis,Plus,mybatis)