mybatis plus 基本使用和源码解析

简介

mybatis-plus是一款mybatis增强工具,用于简化开发,提高效率。mybatis-plus免去了用户编写sql的麻烦,只需要创建好实体类,并创建一个继承自BaseMapper的接口,mybatis就可以自动生成关于单表的crud。mybatis-plus自动做了下划线命名到驼峰命名之间的转换,并且会根据实体类中的信息动态地生成sql,还支持通过java代码来编写sql。

需要注意的是,mybatis plus只可以生成单表查询相关的sql,对于多表查询,mybatis plus没有什么好的解决方案。

入门案例

这是一个springboot整合mybatis plus的入门案例,这几乎是mybatis plus最常见的使用场景了,毕竟没人会单独使用mybatis plus。这里只展示mybatis plus相关的代码,springboot相关的操作不展示

第一步:添加依赖

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-extension</artifactId>
    <version>3.5.1</version>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-annotation</artifactId>
    <version>3.5.1</version>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.1</version>
</dependency>

第二步:springboot中关于mybatis plus的配置

properties 复制代码
# 1. 配置mapper.xml文件的位置,这是默认配置,如果没有这个配置,mybatis plus默认会去
# mapper目录下寻找xml配置文件,建议还是配置上,语义更明确
mybatis-plus.configuration.mapper-locations=classpath:/mapper/*.xml

# 2. 打印mybatis-plus日志的两种方式:
#  方式1:直接打印到控制台
#mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

#  方式2:打印到日志文件中,这里配置完之后还需要在log4j2的配置文件中配置自定义Logger
logging.level.com.baomidou.mybatisplus=DEBUG
logging.level.org.wyj.blog.mapper=DEBUG
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl

# 3. 配置表名的统一前缀,比如,有些项目组,喜欢把表名前缀加 t_,视图前缀加 v_,使用这个配置就会很方便
mybatis-plus.global-config.db-config.table-prefix=t_

# 4. 全局主键策略为自增,避免多次在单个实体类中声明
mybatis-plus.global-config.db-config.id-type=auto

# 数据库连接池等其他组件,正常配置

第三步:mybatis plus的配置类

java 复制代码
@Configuration
@MapperScan("org.wyj.mapper")  // 配置mapper接口的位置
public class MybatisPlusConfig {
    // 分页插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

第四步:mapper接口和实体类

  • 编写实体类:实体类和数据表是对应的,注意,这里使用自增ID,mybatis-plus默认使用雪花算法生成ID
  • 编写mapper接口:mapper接口只需要继承BaseMapper接口即可
java 复制代码
// mapper接口:位于MapperScan注解指定的目录下,继承BaseMapper,指定实体类作为BaseMapper的泛型参数
public interface UserMapper extends BaseMapper<User> {
}

// 和数据库表对应的实体类
@Data
public class User {
    @TableId(type = IdType.AUTO) // 使用数据库自增ID
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

第五步:编写mapper.xml。案例中是一个单表查询的sql。这个案例其实不太好,之所以需要编写xml文件,是因为mybatis plus无法处理多表查询,对于单表查询,即使是复杂的sql,mybatis plus也可以搞定,不过这里主要是为了演示一下使用mybatis plus是如何配置mapper.xml文件的位置,读者只要知道这一点就好。

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 2.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.wyj.mapper.UserMapper">

    <resultMap id="baseMap" type="org.wyj.entity.User">
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="name" property="name" jdbcType="VARCHAR" />
        <result column="age" property="age" jdbcType="VARCHAR" />
        <result column="email" property="email" jdbcType="VARCHAR" />
    </resultMap>

    <sql id="column">
        id, name, age, email
    </sql>

    <!--根据id列表查询用户列表-->
    <select id="findUserByIdList" resultMap="baseMap">
        SELECT <include refid="column" />
        FROM user
        WHERE id IN
        <foreach collection="list" open="(" close=")" separator="," item="id">
            #{id}
        </foreach>
    </select>
</mapper>

第五步:测试,针对单表的crud。用户没有编写相关sql,使用的是mybatis plus提供的能力

java 复制代码
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {InitApplication.class})
public class UserMapperTest {
    @Autowired
    private UserMapper userMapper;

    // 测试selectList方法
    @Test
    public void test1() {
        List<User> userList = userMapper.selectList(null);
        Assert.assertEquals(5, userList.size());
        userList.forEach(System.out::println);
    }

    // 测试insert方法
    @Test
    public void test2() {
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        user.setEmail("zs@163.com");
        int i = userMapper.insert(user);
        assert i == 1;
    }

    // 测试分页插件
    @Test
    public void test3() {
        IPage<User> userIPage = userMapper.selectPage(new Page<>(0, 3), null);
        List<User> records = userIPage.getRecords();
        assert records.size() == 3;
        records.forEach(System.out::println);
    }

    // 测试查询条件构造器,查询ID等于6的数据
    @Test
    public void test4() {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getId, 6);
        User user = userMapper.selectOne(queryWrapper);
        assert user != null;
        System.out.println("user = " + user);
    }
}

在测试案例中演示了如何使用mybatis plus来编写sql,主要有几点:

  • insert语句:只需要提供一个实体类,mybatis plus就可以根据实体类生成对应表的insert语句
  • select语句:在案例中,使用LambdaQueryWrapper,可以通过java代码来编写复杂的sql,只要熟悉sql,这些api相信会很熟练。update、delete语句也类似
  • 分页语句:分页查询是实际开发中比较复杂的点,相较于普通的crud。分页查询内部需要两条sql,一条计算总条数,一条获取当前分页的数据。使用mybatis plus提供的分页查询能力之前,需要先配置分页插件。

总结:案例中展示了mybatis的基本使用,这是springboot接入mybatis plus需要做的最基本的配置。

基本使用

继承BaseMapper,实现单表基本crud的功能

用户编写的Mapper接口只需要继承BaseMapper,同时在泛型中指定数据表对应的实体类,就可以获得基本的增删改查能力

案例:用户编写的UserMapper,继承BaseMapper,同时在泛型中指定实体类User,它定了实体类和数据表的对应关系,现在用户就可以使用UserMapper来操作user表了

java 复制代码
public interface UserMapper extends BaseMapper<User> {
    List<User> findUserByIdList(@Param("list") List<Long> list);
}

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

1、查询一个列表:selectList方法

java 复制代码
@Test
public void test1() {
    List<User> users = userMapper.selectList(null);
    assert users != null && !users.isEmpty();
}

2、查询单条数据: selectById,根据id查询数据,selectOne,根据指定条件,只查询一条数据,例如,根据唯一索引来查询某个值。

java 复制代码
@Test
public void test2() {
    User user = userMapper.selectById(1L);
    assert user != null;
}

3、新增数据:insert方法,mybatis plus会自动进行id回填

java 复制代码
@Test
public void test3() {
    User user = new User();
    user.setName("abc");
    user.setAge(20);
    user.setEmail("aa@111.com");

    int insertNum = userMapper.insert(user);
    assert insertNum == 1;
    assert user.getId() != null; // id自动回填
}

4、修改数据:updateById方法,这里有一个好习惯,就是构建数据时,只构建要被修改的字段,业务语义更明确。

java 复制代码
@Test
public void test4() {
    // 准备数据
    long id = 1L;
    String email = "bbb@111.com";

    User user = new User();
    user.setId(id);
    user.setEmail(email);

    // 修改
    int updateNum = userMapper.updateById(user);
    assert updateNum == 1;

    // 验证
    User userFromDB = userMapper.selectById(id);
    assert userFromDB != null && email.equals(userFromDB.getEmail());
}

5、删除数据:deleteById方法

java 复制代码
@Test
public void test5() {
    int deleteNum = userMapper.deleteById(1L);
    assert deleteNum == 1;
}

上面的几个案例,演示了基本的crud操作,只需要继承BaseMapper,用户就无需再编写复杂的sql。

构建复杂的查询条件 LambdaQueryWrapper

案例:编写一条sql,查询年龄大于20岁、有email、并且名称中包含指定字符的用户,只需要id、name字段

java 复制代码
public List<User> getUsersOver20WithEmailAndNameLike(String nameKeyword) {
    LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
    // 选择指定字段
    queryWrapper.select(User::getId, User::getName, User::getAge, User::getEmail)
            // 条件1:年龄大于20岁
            .gt(User::getAge, 20)
            // 条件2:email不为空
            .isNotNull(User::getEmail)
            // 条件3:名称包含指定字符(模糊查询)
            .like(StringUtils.isNotBlank(nameKeyword), User::getName, nameKeyword);
    return userMapper.selectList(queryWrapper);
}

在这个方法中,通过Java代码来构建sql语句中的查询条件,只要熟悉sql的编写,相信这个方法不难理解。它指定了要查询的字段,指定了查询条件,同时还有类似于sql标签的功能,比起手动编写sql,这种方式的效率可以更高,因为手动编写sql,如果有一些简单的语法错误,要在运行阶段才可以看出来,通过这种方式基本避免了这种错误。

构建复杂的更新条件 LambdaUpdateWrapper

案例:根据用户的姓名更新邮箱,假设用户名称在表中有唯一索引

java 复制代码
// 根据用户的姓名更新邮箱
public int updateEmailByName(User user) {
    // 非空校验
    if (user == null 
            || StringUtils.isBlank(user.getName()) 
            || StringUtils.isBlank(user.getEmail())) {
        return 0;
    }

    // 更新
    LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
    wrapper.set(User::getEmail, user.getEmail());
    wrapper.eq(User::getName, user.getName());
    return userMapper.update(user, wrapper);
}

// 测试
@Test
public void test7() {
    // 构建数据
    String name = "aaa";
    String email = "aaa@111.com";
    User user = new User();
    user.setName(name);
    user.setEmail(email);

    // 测试
    int updateNum = this.updateEmailByName(user);
    assert updateNum != 0;

    // 验证
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(User::getName, name);
    List<User> users = userMapper.selectList(wrapper);
    assert !users.isEmpty();
    for (User u : users) {
        assert email.equals(u.getEmail());
    }
}

sql片段 setSql方法

在某些场景下,需要数据库中的字段自更新,而不是依赖实体类中的值,例如,版本号加1,这个时候,就需要指定sql片段

案例: 更新表中记录的状态,并且版本号加1

java 复制代码
LambdaUpdateWrapper<BookDO> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(BookDO::getState, state)
        .setSql("version = version + 1")
        .set(BookDO::getUpdatePin, userPin)
        .set(BookDO::getUpdateTime, new Date())
        .eq(BookDO::getDeleteFlag, 0)
        .in(BookDO::getId, idList);  

分页查询

普通的分页查询功能,通常需要两条sql,一条查询总条数,一条查询分页数据,使用mybatis plus,只需要配置一个分页插件,myatis plus就可以在一条普通sql上添加分页功能。

具体事项:

第一步:配置分页插件

java 复制代码
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
    mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
    return mybatisPlusInterceptor;
}

第二步:调用selectPage方法

java 复制代码
@Test
public void test8() {
    Page<User> page = new Page<>(1L, 10);

    User user = new User();
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    if (StringUtils.isNotBlank(user.getName())) {
        wrapper.like(User::getName, user.getName());
    }
    if (user.getAge() != null) {
        wrapper.eq(User::getAge, user.getAge());
    }
    if (StringUtils.isNotBlank(user.getEmail())) {
        wrapper.eq(User::getEmail, user.getEmail());
    }
    wrapper.orderByDesc(User::getId);

    Page<User> pageResult = userMapper.selectPage(page, wrapper);
    List<User> records = pageResult.getRecords();
    assert pageResult.getTotal() != 0;
    assert !records.isEmpty();
}

配置id的生成方式

mybatis plus默认使用雪花算法来生成id,同时还支持其它配置方式。

配置id的使用方式:

方式1:配置某张表使用自增id

java 复制代码
@Data
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

方式2:全局配置,使用自增id

配置1:

properties 复制代码
mybatis-plus.global-config.db-config.id-type=auto

配置2:

java 复制代码
@Configuration
public class MybatisPlusConfig {

    // 添加全局配置,使用自增id
    @Bean
    public GlobalConfig globalConfig() {
        GlobalConfig globalConfig = new GlobalConfig();
        GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
        dbConfig.setIdType(IdType.AUTO);
        globalConfig.setDbConfig(dbConfig);
        return globalConfig;
    }
}

任选一种配置即可。

总结:方式1的优先级大于方式2

源码解析

springboot整合mybatis-plus的相关源码

回顾一下springboot整合mybatis的方式

mybatis提供的启动器,会向spring容器中注入两个bean,SqlSessionFactory、SqlSessionTemplate,SqlSessionFactory是创建SqlSession的工厂类,SqlSession在mybatis中代表和数据库的一次会话,是mybatis的核心抽象,SqlSessionTemplate在SqlSession外面又包了一层,它负责在调用SqlSession时进行某些额外操作,例如清理缓存等。

用户还需要使用@MapperScan注解,指定mapper接口的所在包,@MapperScan注解会向容器中导入一个注册器,这个注册器会扫描指定包下的所有接口,把它们视为Mapper接口,注入到spring容器中,还会修改这些接口的bean信息,把接口的类对象替换为MapperFactoryBean.class,使用一个工厂类创建Mapper接口的代理类,这个工厂类还会获取之前注入的SqlSessionTemplate,通过它来执行sql。

这就是springboot整合mybatis的大致流程,接下来看一下springboot是如何整合mybatis plus的。

mybatis plus提供的启动器

springboot整合mybatis-plus,需要依赖mybatis-plus提供的启动器:mybatis-plus-boot-starter

启动器的maven坐标:

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.2.0</version>
</dependency>

启动器中的依赖项:依赖mybatis plus

xml 复制代码
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus</artifactId>
  <version>3.5.1</version>
  <scope>compile</scope>
</dependency>

mybatis plus的依赖项:依赖 mybatis-plus-extension

xml 复制代码
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-extension</artifactId>
  <version>3.5.1</version>
  <scope>compile</scope>
</dependency>

mybatis-plus-extension的依赖项:

xml 复制代码
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-core</artifactId>
  <version>3.5.1</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>2.0.6</version>
  <scope>compile</scope>
</dependency>

mybatis-spring的依赖项:

xml 复制代码
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.6</version>
  <scope>provided</scope>
</dependency>

总结:这里只介绍几个重要的依赖项,可以看到,用户只要依赖了mybatis plus的启动器,它就会自动引用mybatis plus、mybatis spring、mybatis等,启动,mybatis spring负责mybatis和spring的整合,mybatis提供的启动器同样会依赖到它。

启动器导入的配置类

打开启动器对应的jar包,打开jar包中的spring.factoies文件,文件中记录了mybatis plus提供的配置类,这是springboot自动装配相关的部分。

properties 复制代码
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration

从spring.factoies文件中可以看到,MybatisPlusAutoConfiguration是spring容器启动时mybatis-plus注入的类

MybatisPlusAutoConfiguration的整体结构

java 复制代码
@Configuration(proxyBeanMethods = false)
// 自动装配的条件,类路径下必须有SqlSessionFactory、SqlSessionFactoryBean,这两个是mybatis中的类,
// spring容器中必须要有数据库连接池,
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
// 解析springboot提供的配置项
@EnableConfigurationProperties(MybatisPlusProperties.class)
// 解析顺序,在数据库连接池之后解析
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {

    private static final Logger logger = LoggerFactory.getLogger(MybatisPlusAutoConfiguration.class);

    // 构造方法,spring容器会根据这个构造方法,把它需要的实例传递给它
    public MybatisPlusAutoConfiguration(MybatisPlusProperties properties,
                                        ObjectProvider<Interceptor[]> interceptorsProvider,
                                        ObjectProvider<TypeHandler[]> typeHandlersProvider,
                                        ObjectProvider<LanguageDriver[]> languageDriversProvider,
                                        ResourceLoader resourceLoader,
                                        ObjectProvider<DatabaseIdProvider> databaseIdProvider,
                                        ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider,
                                        ObjectProvider<List<MybatisPlusPropertiesCustomizer>> mybatisPlusPropertiesCustomizerProvider,
                                        ApplicationContext applicationContext) {
        this.properties = properties;
        this.interceptors = interceptorsProvider.getIfAvailable();
        this.typeHandlers = typeHandlersProvider.getIfAvailable();
        this.languageDrivers = languageDriversProvider.getIfAvailable();
        this.resourceLoader = resourceLoader;
        this.databaseIdProvider = databaseIdProvider.getIfAvailable();
        this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
        this.mybatisPlusPropertiesCustomizers = mybatisPlusPropertiesCustomizerProvider.getIfAvailable();
        this.applicationContext = applicationContext;
    }

    // 当前类实现了InitializingBean接口,这是实例化后的扩展点,用于检测配置文件是否存在,如果用户指定了的话
    @Override
    public void afterPropertiesSet() {
        if (!CollectionUtils.isEmpty(mybatisPlusPropertiesCustomizers)) {
            mybatisPlusPropertiesCustomizers.forEach(i -> i.customize(properties));
        }
        checkConfigFileExists();
    }

    private void checkConfigFileExists() {
        if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
            Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
            Assert.state(resource.exists(),
                "Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)");
        }
    }

    // 注入bean,SqlSessionFactory,随后讲解
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // 省略代码
    }

    // 注入bean,SqlSessionTemplate,
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        ExecutorType executorType = this.properties.getExecutorType();
        if (executorType != null) {
            return new SqlSessionTemplate(sqlSessionFactory, executorType);
        } else {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }


    // 一个内部配置类,通常,如果用户没有使用@MapperScan注解,就会引入这个配置类,
    // 因为@MapperScan注解会引入MapperScannerConfigurer,扫描Mapper接口,
    // 这个配置类会引入一个扫描Mapper接口的组件,它会从spring容器的basePackage下,
    // 扫描所有被@Mapper接口标注的接口
    @Configuration(proxyBeanMethods = false)
    @Import(AutoConfiguredMapperScannerRegistrar.class)
    @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
    public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

        @Override
        public void afterPropertiesSet() {
            logger.debug(
                "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
        }
    }
    
    // 上面的内部配置类导入的注册器
    public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {

        private BeanFactory beanFactory;

        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

           // 省略代码
    }

}

总结:启动器导入的配置类中,向spring容器中注入了SqlSessionFactory、SqlSessionTemplate,如果用户没有使用@MapperScan注解的话,还会额外注入一个扫描Mapper接口的注册器 。

配置类导入的bean

1、SqlSessionFactory

java 复制代码
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    // TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean
    MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    
    // ... 省略代码,mybatis相关的特性,拦截器、类型处理器等
    
    // TODO 此处必为非 NULL
    GlobalConfig globalConfig = this.properties.getGlobalConfig();
    // TODO 注入填充器
    this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
    // TODO 注入主键生成器
    this.getBeansThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerators(i));
    // TODO 注入sql注入器
    this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
    // TODO 注入ID生成器
    this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
    // TODO 设置 GlobalConfig 到 MybatisSqlSessionFactoryBean
    factory.setGlobalConfig(globalConfig);
    return factory.getObject();
}

上面这段源码中的中文注释都是自带的,因为mybatis plus是国内公司开发的,需要注意的是sql注入器、globalConfig,sql注入器就是mybatis plus负责自动生成sql的组件,globalConfig是全局配置。SqlSessionFactory是mybatis中的组件,它持有配置类的实例,mybatis plus提供了自己的配置类并且把它注入到了SqlSessionFactory中,这就是mybatis plus可以在mybatis的基础上增加某些功能的原因。mybatis提供的配置类是mybatis的配置类的子类。

mybatis plus中提供的配置类:

java 复制代码
public class MybatisConfiguration extends Configuration {
    private static final Log logger = LogFactory.getLog(MybatisConfiguration.class);
    /**
     * Mapper 注册,这里就是自动注入相关的逻辑
     */
    protected final MybatisMapperRegistry mybatisMapperRegistry = new MybatisMapperRegistry(this);

    // 添加Mapper接口
    @Override
    public <T> void addMapper(Class<T> type) {
        mybatisMapperRegistry.addMapper(type);
    }

}

顺着这个配置类,就可以找到自动注入相关的逻辑。mybatis plus重写了mybatis解析Mapper接口的逻辑,包括Mapper接口中可能存在的注解,例如@Select等。

这里直接看自动注入相关的逻辑,不再看具体流程,以注入selectList方法为例:

java 复制代码
public class SelectList extends AbstractMethod {

    public SelectList() {
        super(SqlMethod.SELECT_LIST.getMethod());
    }

    /**
     * @param name 方法名
     * @since 3.5.0
     */
    public SelectList(String name) {
        super(name);
    }

    // 参数1是Mapper接口的类对象,参数2是实体类的类对象,参数3是根据实体类解析出的表信息,包括表名、表字段、主键信息
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        // 配置在枚举类中的常量,SELECT_LIST("selectList", "查询满足条件所有数据", "<script>%s SELECT %s FROM %s %s %s %s\n</script>"), 
        SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
        // 这里就是拼接sql模板,注意看上面sql语句中的占位符,这里会把字段名、表名等信息拼接到sql语句中,
        // 随后会根据LambdaQueryWrapper,向这个sql模板中填充where条件,
        String sql = String.format(sqlMethod.getSql(), sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(),
            sqlWhereEntityWrapper(true, tableInfo), sqlOrderBy(tableInfo), sqlComment());
        // 把sql注入到SqlSource中,这是mybatis中的组件
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
        return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo);
    }
}

生成的sql案例:这是以之前案例中的User类为例,生成的selectList查询语句,随后会根据LambdaQueryWrapper表达式,向当前sql模板中注入查询条件

xml 复制代码
<script>
    <if test="ew != null and ew.sqlFirst != null">
        ${ew.sqlFirst}
    </if> 
    SELECT 
    <choose>
        <when test="ew != null and ew.sqlSelect != null">
            ${ew.sqlSelect}
        </when>
        <otherwise>
            id,name,age,email
        </otherwise>
    </choose> 
    FROM user 
    <if test="ew != null">
        <where>
            <if test="ew.entity != null">
                <if test="ew.entity.id != null">
                    id=#{ew.entity.id}
                </if>
                <if test="ew.entity['name'] != null"> 
                    AND name=#{ew.entity.name}
                </if>
                <if test="ew.entity['age'] != null"> 
                    AND age=#{ew.entity.age}
                </if>
                <if test="ew.entity['email'] != null"> 
                    AND email=#{ew.entity.email}
                </if>
            </if>
            <if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere">
                <if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal"> 
                    AND
                </if> 
                ${ew.sqlSegment} 
            </if>
        </where>
        <if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere">
            ${ew.sqlSegment}
        </if>
    </if>  
    <if test="ew != null and ew.sqlComment != null">
        ${ew.sqlComment}
    </if>
</script>

其它默认的语句,例如insert、delete,也类似,根据实体类信息,解析出数据表信息,然后根据预先定义好的sql模板,这些sql'模板被定义在枚举类中,然后向模板中注入表名、字段名等信息,然后预留占位符,随后根据LambdaQueryWrapper、LambdaUpdateWrapper等,进一步拼接条件,最终形成一条可执行的sql。

一共注入了哪些方法:

java 复制代码
// 默认的sql注入器,这里暂不关心它在哪里被调用,只关注它注入了哪些sql方法
public class DefaultSqlInjector extends AbstractSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
        Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>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 {
            logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
                tableInfo.getEntityType()));
        }
        return builder.build().collect(toList());
    }
}

总结:这里介绍了注入SqlSessionFactory相关的逻辑,mybatis plus重写了mybatis的配置类,并且把它注入到了SqlSessionFactory中。在spring容器实例化,创建Mapper接口的代理类的过程中,最终会通过SqlSessionFactory,调用mybatis plus提供的配置类,创建Mapper接口的代理类,在这个过程中,会为BaseMapper接口中定义的方法创建相应的sql语句,并且把它们加入到Mapper接口的代理类中。sql语句的相关信息被包装在MappedStatement实例中。这里只展示部分关键信息,因为大部分是mybatis、springboot中的概念,而且对mybatis plus中的流程也做了省略。

2、注入SqlSessionTemplate,这里就比较简单了,它是SqlSession的代理类,和mybatis中的一样

java 复制代码
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    ExecutorType executorType = this.properties.getExecutorType();
    if (executorType != null) {
        return new SqlSessionTemplate(sqlSessionFactory, executorType);
    } else {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

LambdaQueryWrapper的执行过程

上一步中,spring容器启动,Mapper接口中的代理类创建成功,并且为BaseMapper中定义的方法生成了sql语句,生成的sql语句中,已经包含了字段名、表名,这两部分的内容,剩下的内容,例如where子句,以占位符的形式存在,接下来就看一下,mybatis plus是如何把LambdaQueryWrapper渲染成where语句的。

LambdaQueryWrapper的继承体系

LambdaQueryWrapper:

java 复制代码
// 泛型T需要传入实体类
public class LambdaQueryWrapper<T> extends AbstractLambdaWrapper<T, LambdaQueryWrapper<T>>
    implements Query<LambdaQueryWrapper<T>, T, SFunction<T, ?>> {
    
    /**
     * 查询字段,select子句后的列名,如果用户有指定,使用用户指定的
     */
    private SharedString sqlSelect = new SharedString();
}

AbstractLambdaWrapper:LambdaQueryWrapper的抽象父类,它的泛型比较复杂,泛型T代表实体类,泛型Children代表子类,所以Children需要继承AbstractLambdaWrapper

java 复制代码
public abstract class AbstractLambdaWrapper<T, Children extends AbstractLambdaWrapper<T, Children>>
    extends AbstractWrapper<T, SFunction<T, ?>, Children> {
 
    // 缓存解析好的字段名,这里会使用TableInfo中已经解析好的数据,不会再解析一次
    private Map<String, ColumnCache> columnMap = null;
    private boolean initColumnMap = false;
}

AbstractWrapper:AbstractLambdaWrapper的抽象父类,这里定义了主要的逻辑

java 复制代码
@SuppressWarnings({"unchecked"})
public abstract class AbstractWrapper<T, R, Children extends AbstractWrapper<T, R, Children>> extends Wrapper<T>
    implements Compare<Children, R>, Nested<Children, Children>, Join<Children>, Func<Children, R> {

    /**
     * 占位符,子类也会继承,用于链式调用
     */
    protected final Children typedThis = (Children) this;
    
    // 存储where子句中的每个部分,包括列名、运算符、值、逻辑运算符
    protected MergeSegments expression;
    
    /**
     * 数据库表映射实体类
     */
    private T entity;
    
    /**
     * 实体类型(主要用于确定泛型以及取TableInfo缓存)
     */
    private Class<T> entityClass;
}

Wrapper:

java 复制代码
public abstract class Wrapper<T> implements ISqlSegment {
    /**
     * 实体对象(子类实现)
     *
     * @return 泛型 T
     */
    public abstract T getEntity();
    
    /**
     * 获取 MergeSegments
     */
    public abstract MergeSegments getExpression();
}

ISqlSegment:代表一个sql片段,可以是列名,也可以是运算符、值,总之是sql语句中被空格分割的一部分,

java 复制代码
@FunctionalInterface
public interface ISqlSegment extends Serializable {

    /**
     * SQL 片段
     */
    String getSqlSegment();

}

总结:从这一些继承体系中可以看出,LambdaQueryWrapper也可以理解为是一个sql片段, 只是它是拼接好的sql片段

拼接sql片段

以like语句为例,查看查询条件是如何被封装的:

java 复制代码
// 这是用户侧的调用案例,like(StringUtils.isNotBlank(nameKeyword), User::getName, nameKeyword),
// 看一下这个语句是如何被转换为sql语句的

// like方法
@Override
public Children like(boolean condition, R column, Object val) {
    // 参数2,like关键字
    // 参数5,SqlLike.DEFAULT,%值%,它会自动在值的两侧拼接通配符
    // 其它参数都是用户传入的,条件、列名、值
    return likeValue(condition, LIKE, column, val, SqlLike.DEFAULT);  
}

// like方法的进一步调用,这里实际上比较难以理解的地方在于,maybeDo方法中,第二个值是一个lambda表达式
protected Children likeValue(boolean condition, SqlKeyword keyword, R column, Object val, SqlLike sqlLike) {
    return maybeDo(condition, () -> 
                   appendSqlSegments(columnToSqlSegment(column), 
                                     keyword, 
                                     () -> formatParam(null, SqlUtils.concatLike(val, sqlLike))));
}

// maybeDo方法,如果条件为true,执行上面第二个参数的lambda表达式
protected final Children maybeDo(boolean condition, DoSomething something) {
    if (condition) {
        something.doIt();
    }
    return typedThis;
}

// appendSqlSegments方法,把sql片段添加到expression中。上面likeValue方法中,appendSqlSegments
// 方法的三个参数,每一个都是ISqlSegement接口的实例,columnToSqlSegment是从方法引用中解析出字段名,
// formatParam是把值拼接为诸如 "#{字段名}" 的逻辑
protected void appendSqlSegments(ISqlSegment... sqlSegments) {
    expression.add(sqlSegments);
}

// formatParam方法,封装占位符 #{} 的逻辑
protected final String formatParam(String mapping, Object param) {
    // 这里会拼接一个占位符,然后再把它和值之间的映射存储起来,占位符类似于 ew.paramNameValuePairs.MPGENVAL1,
    // 最后的1是原子类自增,
    
    // 占位符, WRAPPER_PARAM = MPGENVAL,
    // getParamAlias() = "ew",WRAPPER_PARAM_MIDDLE = ".paramNameValuePairs" + DOT;
    // 注意paramAlias,它的值是ew,这是mybatis plus中默认的,selectList方法被@Param注解标注,
    // @Param注解指定参数名是ew,而LambdaQueryWrapper的实例又是传递给selectList方法的实参,
    // 所以"ew"会和LambdaQueryWrapper的实例绑定到一起。
    final String genParamName = Constants.WRAPPER_PARAM + paramNameSeq.incrementAndGet();
    final String paramStr = getParamAlias() + Constants.WRAPPER_PARAM_MIDDLE + genParamName;
    // 占位符和值的映射
    paramNameValuePairs.put(genParamName, param);
    // 拼接 #{占位符}
    return SqlScriptUtils.safeParam(paramStr, mapping);
}

总结:以like方法为例,它生成like语句相关的sql片段,最终依次存储到expression中,sql片段包括 列名、like关键字、#{占位符},随后会按照顺序把它们拼接在一起。

这里只展示了外部的调用路径,核心部分在随后的拼接过程中

关键字的拼接逻辑

关键字被组织在枚举类中,并且枚举类实现了ISqlSegment接口,这可以学习一下枚举类继承某个接口的实际应用

关键字:

java 复制代码
public enum SqlKeyword implements ISqlSegment {
    AND("AND"),
    OR("OR"),
    IN("IN"),
    NOT("NOT"),
    LIKE("LIKE"),
    EQ(StringPool.EQUALS),
    NE("<>"),
    GT(StringPool.RIGHT_CHEV),
    GE(">="),
    LT(StringPool.LEFT_CHEV),
    LE("<="),
    IS_NULL("IS NULL"),
    IS_NOT_NULL("IS NOT NULL"),
    GROUP_BY("GROUP BY"),
    HAVING("HAVING"),
    ORDER_BY("ORDER BY"),
    EXISTS("EXISTS"),
    BETWEEN("BETWEEN"),
    ASC("ASC"),
    DESC("DESC");

    private final String keyword;

    SqlKeyword(final String keyword) {
        this.keyword = keyword;
    }

    @Override
    public String getSqlSegment() {
        return this.keyword;
    }
}
根据get方法的方法引用解析出字段名

在整体流程中,这一个环节有必要单独拿出来介绍一下。

通过SFunction来接收方法引用,例如 User::getName,然后进一步解析。

SFunction: 用于接收诸如 User::getName 等get方法,mybatis plus会从这些lambda表达式中解析出字段名,这是Java提供的能力,

java 复制代码
@FunctionalInterface
public interface SFunction<T, R> extends Function<T, R>, Serializable {
}

// Function接口,接收一个参数,返回一个结果
public interface Function<T, R> {
    R apply(T t);
}

从方法引用中解析出字段名:

java 复制代码
// 参数,方法引用,例如 User::getName
protected ColumnCache getColumnCache(SFunction<T, ?> column) {
    // 这里使用了Java提供的能力,把lambda表达式解析到 SerializedLambda 实例中
    LambdaMeta meta = LambdaUtils.extract(column);
    // 根据get方法,解析出字段名
    String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName());
    Class<?> instantiatedClass = meta.getInstantiatedClass();
    // 这里根据实体类的类对象,获取之前解析好的缓存,这是在spring容器启动时,根据实体类解析TableInfo时做的,
    tryInitCache(instantiatedClass);
    // 获取ColumnCache,它包括实体类中的字段名、表中的列名
    return getColumnCache(fieldName, instantiatedClass);
}

// 根据get方法,解析字段名的逻辑
public static String methodToProperty(String name) {
  if (name.startsWith("is")) {
    name = name.substring(2);
  } else if (name.startsWith("get") || name.startsWith("set")) {
    name = name.substring(3);
  } else {
    throw new ReflectionException("Error parsing property name '" + 
                                  name + "'.  Didn't start with 'is', 'get' or 'set'.");
  }

  if (name.length() == 1 || (name.length() > 1 && !Character.isUpperCase(name.charAt(1)))) {
    name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
  }

  return name;
}

实际演示一下如何从User::getEmail中解析出getName的方法名称

java 复制代码
// 第一步:自定义函数式接口,注意这个接口一定要支持序列化
@FunctionalInterface
public interface MyFunc<T, R> extends Function<T, R>, Serializable {
}


// 第二步:解析方法名
@Test
public void test1() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    MyFunc<User, String> func = User::getEmail;
    Method method = func.getClass().getDeclaredMethod("writeReplace");
    method.setAccessible(true);
    SerializedLambda lambdaMetadata = (SerializedLambda) method.invoke(func);
    
    System.out.println("lambdaMetadata = " + lambdaMetadata);
    String implMethodName = lambdaMetadata.getImplMethodName();
    System.out.println("implMethodName = " + implMethodName);  // getEmail 
}

原理讲解:writeReplace 方法是 Java 序列化机制中的一个特殊方法。如果一个对象在序列化前定义了此方法,那么序列化时会调用该方法,并序列化它返回的对象,而不是原始对象本身。SerializedLambda 是 JDK 为支持序列化 Lambda 表达式而引入的类。当一个可序列化的 Lambda 表达式,例如,实现了 Serializable 的接口,被序列化时,JDK 会通过内部的 writeReplace 方法将其转换成一个 SerializedLambda 对象,这个对象包含了捕获该 Lambda 所需的所有信息,如实现方法、目标类、捕获的参数等。

mybatis plus正式通过这种机制,从方法引用中解析出方法名,进而进一步解析出字段名

拼接过程

sql案例:

java 复制代码
public List<User> queryByCondition() {
    LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();

    lambdaQueryWrapper
            .notIn(User::getId, Lists.newArrayList(2000L, 3000L, 4000L))   
            .like(User::getName, "a")
            .gt(User::getAge, "20")
            .and(wrapper -> wrapper
                    .isNotNull(User::getEmail)
                    .or()
                    .lt(User::getId, 1000L))
            // 先根据age正序排序,然后根据id倒序排序
            .orderByAsc(User::getAge)
            .orderByDesc(User::getId);
    return userMapper.selectList(lambdaQueryWrapper);
}

这是一条尽可能复杂的sql,接下来了解一下mybatis plus是如何把它转换为sql语句的。有一点我一直没有注意到,直到尝试了解mybatis plus的源码时才发现,例如,在notIn和like之间,应该有一个and,显然,是mybatis plus内部做了这种拼接,所以它是如何处理的?

mybatis plus在把java代码转换为sql语句时,使用了模板方法设计模式,并且使用了继承、组合等多种方式,来组织内部的组件,我接下来会把所有代码都放在一起,但是我会标清它的执行步骤,希望可以讲清楚。

整体流程: MergeSegments,整合sql片段,之前提到会把sql片段的集合保存到expression中,这里就是保存时内部的逻辑,expression就是一个MergeSegments实例

java 复制代码
// MergeSegments,保存where子句后面包括普通查询条件、group by、order by等子句的sql片段
@Getter
@SuppressWarnings("serial")
public class MergeSegments implements ISqlSegment {

    // where 子句
    private final NormalSegmentList normal = new NormalSegmentList();
    // group by子句
    private final GroupBySegmentList groupBy = new GroupBySegmentList();
    // having 子句
    private final HavingSegmentList having = new HavingSegmentList();
    // order by子句
    private final OrderBySegmentList orderBy = new OrderBySegmentList();

    @Getter(AccessLevel.NONE)
    private String sqlSegment = StringPool.EMPTY;

    // 只要计算过一次,这个值就是true,下次直接走缓存
    @Getter(AccessLevel.NONE)
    private boolean cacheSqlSegment = true;

    // 第一步:添加sql片段
    public void add(ISqlSegment... iSqlSegments) {
        // 把可变参转换成列表,并且获取列表中的第一个元素,
        List<ISqlSegment> list = Arrays.asList(iSqlSegments);
        ISqlSegment firstSqlSegment = list.get(0);
        // 根据第一个元素的值,来决定sql片段是普通的where子句,还是group by、order by等,不同子句走不同分支
        if (MatchSegment.ORDER_BY.match(firstSqlSegment)) { 
            orderBy.addAll(list); // order by
        } else if (MatchSegment.GROUP_BY.match(firstSqlSegment)) {
            groupBy.addAll(list); // group by
        } else if (MatchSegment.HAVING.match(firstSqlSegment)) {
            having.addAll(list);  // having
        } else {
            normal.addAll(list);  // where 子句
        }
        cacheSqlSegment = false;
    }

    // 最后一步:拼接最终结果
    @Override
    public String getSqlSegment() {
        if (cacheSqlSegment) {
            return sqlSegment;
        }
        cacheSqlSegment = true;
        if (normal.isEmpty()) {
            if (!groupBy.isEmpty() || !orderBy.isEmpty()) {
                sqlSegment = groupBy.getSqlSegment() + having.getSqlSegment() + orderBy.getSqlSegment();
            }
        } else {
            sqlSegment = normal.getSqlSegment() + groupBy.getSqlSegment() + having.getSqlSegment() + orderBy.getSqlSegment();
        }
        return sqlSegment;
    }
}

where子句的拼接: 这里采用了模板方法设计模式,父类设计流程,子类重写某些关键步骤,

java 复制代码
// 抽象父类,父类继承了ArrayList,并且只可以存储ISqlSegment类型的元素,同时实现了ISqlSegment接口,
// StringPool是一个存储常量的接口。把常量写在接口中,比写一个专门的常量类也许更合适吧!毕竟接口中的
// 常量默认被public static final修饰。
public abstract class AbstractISegmentList extends ArrayList<ISqlSegment> implements ISqlSegment, StringPool {

    /**
     * 当前这个类会存储所有的sql片段,这个成员变量是存储集合中的最后一个值,
     * 最后一个值之所以要单独存储,是为了处理sql片段之间拼接and、or的逻辑
     */
    ISqlSegment lastValue = null;
    boolean flushLastValue = false;
    // 缓存计算结果,一个LambdaQueryWrapper实例,只要计算一次,结果就不会再变了。
    private String sqlSegment = EMPTY;
    private boolean cacheSqlSegment = true;


    // 第二步:这里就是第一步中where子句的分支进入的方法,添加普通的where条件到集合中
    @Override
    public boolean addAll(Collection<? extends ISqlSegment> c) {
        List<ISqlSegment> list = new ArrayList<>(c);
        // 第三步:子类重写transformList方法,先处理sql片段,例如,
        // 普通的where子句,会在sql片段的最后拼接and,group by子句,会把group by前缀去掉
        boolean goon = transformList(list, list.get(0));
        if (goon) {
            // 第四步:子类处理完集合后,如果需要把sql片段拼接到最后结果中,就返回true,然后进入当前分支,
            // 拼接sql片段,刷新列表的最后一个值
            cacheSqlSegment = false;
            if (flushLastValue) {
                this.flushLastValue(list);
            }
            return super.addAll(list);
        }
        return false;
    }

    // 子类重写这个方法,这里会改变list内部的元素
    protected abstract boolean transformList(List<ISqlSegment> list, ISqlSegment firstSegment);

    // 把列表中最后一个元素保存到成员变量中
    private void flushLastValue(List<ISqlSegment> list) {
        lastValue = list.get(list.size() - 1);
    }

    // 
    void removeAndFlushLast() {
        remove(size() - 1);
        flushLastValue(this);
    }

    // 拼接最终结果
    @Override
    public String getSqlSegment() {
        if (cacheSqlSegment) {
            return sqlSegment;
        }
        cacheSqlSegment = true;
        sqlSegment = childrenSqlSegment();
        return sqlSegment;
    }

    protected abstract String childrenSqlSegment();
}

负责拼接where子句的类: NormalSegmentList

java 复制代码
public class NormalSegmentList extends AbstractISegmentList {

    /**
     * 是否处理了的上个 not
     */
    private boolean executeNot = true;

    NormalSegmentList() {
        this.flushLastValue = true;
    }

    // 第三步进入的方法:这个方法中,主要是负责处理and、or、not关键字,在两个查询条件中拼接and、or,或者移除最后一个and、or
    @Override
    protected boolean transformList(List<ISqlSegment> list, ISqlSegment firstSegment) {
        if (list.size() == 1) {
            /* 只有 and() 以及 or() 以及 not() 会进入 */
            if (!MatchSegment.NOT.match(firstSegment)) {
                // 处理 and、or
                if (isEmpty()) {
                    //sqlSegment是 and 或者 or 并且在第一位,不继续执行
                    return false;
                }
                boolean matchLastAnd = MatchSegment.AND.match(lastValue);
                boolean matchLastOr = MatchSegment.OR.match(lastValue);
                if (matchLastAnd || matchLastOr) {
                    // 上次最后一个值是 and 或者 or,那么不需要处理
                    if (matchLastAnd && MatchSegment.AND.match(firstSegment)) {
                        return false;
                    } else if (matchLastOr && MatchSegment.OR.match(firstSegment)) {
                        return false;
                    } else {
                        // 和上次的不一样,刷新列表的最后一位
                        removeAndFlushLast();
                    }
                }
            } else {
                // 处理not,在下一次调用时,向sql片段中拼接not关键字
                executeNot = false;
                return false;
            }
        } else {
            if (!executeNot) {
                // 下一次调用时会进入这里,处理not,包括 not in、not exists,这里对于not的处理不容易理解,
                // 举个例子,用户进行如下调用 notIn(User::getId, Lists.newArrayList(2000L, 3000L, 4000L)),
                // notIn的内部会转换成 not(condition).in(condition, column, coll) ,所以先处理not关键字,
                // 它会把executeNot改成false,然后返回false,表示现在不需要拼接not,在下一步处理in关键字时,
                // 发现前面需要拼接not,然后就会进入当前分支,在in的前面拼接一个not。
                list.add(MatchSegment.EXISTS.match(firstSegment) ? 0 : 1, SqlKeyword.NOT);
                executeNot = true;
            }
            // 如果当前集合不为空并且最后一个sql片段不是and,在sql片段的最后拼接一个and,这是最常见的情况
            if (!MatchSegment.AND_OR.match(lastValue) && !isEmpty()) {
                add(SqlKeyword.AND);
            }
            if (MatchSegment.APPLY.match(firstSegment)) {
                list.remove(0);
            }
        }
        return true;
    }

    // 遍历sql片段,获取其中的值,这里就是执行之前传入的lambda表达式,获取列名、关键字、字段占位符的字符串形式
    @Override
    protected String childrenSqlSegment() {
        if (MatchSegment.AND_OR.match(lastValue)) {
            removeAndFlushLast();
        }
        final String str = this.stream().map(ISqlSegment::getSqlSegment).collect(Collectors.joining(SPACE));
        return (str.startsWith(LEFT_BRACKET) && str.endsWith(RIGHT_BRACKET)) ? str : (LEFT_BRACKET + str + RIGHT_BRACKET);
    }
}

负责拼接order by子句的类: NormalSegmentList,可以和拼接普通where条件的方法一起对照着看一下

java 复制代码
public class OrderBySegmentList extends AbstractISegmentList {

    // 处理order by相关的sql片段时,会移除order by关键字
    @Override
    protected boolean transformList(List<ISqlSegment> list, ISqlSegment firstSegment) {
        list.remove(0);
        if (!isEmpty()) {  // 这里是判断最终结果集不为空,拼接逗号,然后再方法外会拼接参数中的list
            super.add(() -> COMMA);
        }
        return true;
    }

    // 获取最终结果
    @Override
    protected String childrenSqlSegment() {
        if (isEmpty()) {
            return EMPTY;
        }
        // 拼接order by后的列名时同时会拼接order by关键字作为前缀
        return this.stream().map(ISqlSegment::getSqlSegment).collect(joining(SPACE, SPACE + ORDER_BY.getSqlSegment() + SPACE, EMPTY));
    }
}

这里,把所有的sql片段拼接好之后,随后,在执行sql的时候,会解析sql语句中的占位符,例如 #{ew.sqlSegment},通过解析对象表达式,然后调用对象中的方法,获取这里拼接好的sql片段。

最终拼接结果:这里做了部分美化,随后会使用mybatis提供的能力,把 "#{}" 之类的参数替换为值,LambdaQueryWrapper中存储了这些参数和实际值的映射关系。

sql 复制代码
SELECT  id,name,age,email  
FROM users 
WHERE (id NOT IN (#{ew.paramNameValuePairs.MPGENVAL1},#{ew.paramNameValuePairs.MPGENVAL2},#{ew.paramNameValuePairs.MPGENVAL3}) 
    AND name LIKE #{ew.paramNameValuePairs.MPGENVAL4} 
    AND age > #{ew.paramNameValuePairs.MPGENVAL5} 
    AND ( (email IS NOT NULL 
        OR id < #{ew.paramNameValuePairs.MPGENVAL6})
    )
) 
ORDER BY age ASC , id DESC 

执行过程

用户调用selectList方法,传入LambdaQueryWrapper,然后执行sql,上面已经看到了selectList方法生成的sql模板,接下来看一下LambdaQueryWrapper是如何被渲染到where子句中的。

接下来的功能完成是依赖myatis中的能力,mybatis plus在之前构建好了selectList方法对应的sql模板,又在LambdaQueryWrapper中设置了查询条件和占位符,接下来会根据sql模板,来解析查询条件中的数据,

这是mybtais中的代码,参数parameterObject就包含了LambdaQueryWrapper,

java 复制代码
@Override
public BoundSql getBoundSql(Object parameterObject) {
  DynamicContext context = new DynamicContext(configuration, parameterObject);
  // 根据参数中的数据,解析sql模板,这里是动态sql的一部分,根据值来决定使用哪些sql片段
  rootSqlNode.apply(context);
  SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
  Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
  // 生成数据库可执行的sql
  SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  context.getBindings().forEach(boundSql::setAdditionalParameter);
  return boundSql;
}

接下来就是mybatis中的功能,和mybatis plus无关了,mybatis plus只是负责根据Java代码生成sql,执行是依赖mybatis提供的能力

总结

作为一个java程序员,日常开发就是写一些增删改查页面,mybatis plus很适合这种场景,因为大部分情况都是单表查询,我们会把用户一次操作需要的数据尽量放到一张表中,而且sql也不复杂,mybatis plus减少了很多样板代码,相当强大。一直很好奇它是怎么做到的,在这里做个简要了解。

mybatis plus重写了mybatis的配置类、以及为Mapper接口创建代理类的逻辑,因为重写了配置类,在通过mybatis的SqlSessionFactory创建Mapper接口的代理类时,才会进入mybatis plus的逻辑。在创建Mapper接口的代理类时,会为BaseMapper接口中的方法创建sql语句,这里使用了mybatis中sql标签技术,sql中的某些部分支持参数替换,例如selectList方法,它的查询条件预留了占位符,在随后,使用LambdaQueryWrapper构建where、group by等条件时,会使用构建好的结果替换占位符,而且这是使用了mybatis的能力,所以mybatis plus相当于mybatis是无侵入的,它完全是在mybatis的基础上增强了某些能力。

使用LambdaQueryWrapper是如何构建查询条件的?首先,sql语句中的每一个节点,无论是列表、关键字,还是值,都被抽象为一个ISqlSegment接口,它负责返回这些节点的字符串形式,然后,AbstractISegmentList,实现了ISqlSegment,并且继承了ArrayList,负责把sql片段组合到一个列表中,而且它进一步做了细化,分出了多个子类,并且使用模板方法设计模式,提取出公共逻辑,例如,普通的where子句,使用NormalSegmentList,处理and、or等关键字的拼接,order by子句,使用OrderBySegmentList,处理order by关键字,和列名之间的逗号,最后,MergeSegments,外部组件只需要把sql片段传给它,由它来负责整体处理拼接逻辑,它持有NormalSegmentList、OrderBySegmentList等实例。所以,在这里,既有继承、也有组合,但是组件之间的职责和架构很清晰。

这里只是简单了解了整体流程,还有很多复杂的细节没有涉及,有时间再深入了解。