简介
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等实例。所以,在这里,既有继承、也有组合,但是组件之间的职责和架构很清晰。
这里只是简单了解了整体流程,还有很多复杂的细节没有涉及,有时间再深入了解。