SpringBoot整合Mybatis-Plus篇
1、概述
因为mybatis-plus不是官方开发的,所以没有提供对应的starter。但是民间有大神,有着对应的提供,那么先去官网上找一下:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.4</version>
</dependency>
然后分析一下pom依赖:
可以看到这里引入了mybatis-spring的整合包,注解等等。以及对应的jdbc操作等等。书库连接池使用的是Hikari等
就代表着我们不需要配置任何的东西就已经可以来操作数据库了。
2、自动配置分析
那么去看一下对应的maven依赖中的autoconfig,找到spring.factories目录下的自动配置
# Auto Configure
org.springframework.boot.env.EnvironmentPostProcessor=\
com.baomidou.mybatisplus.autoconfigure.SafetyEncryptProcessor
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.mybatisplus.autoconfigure.IdentifierGeneratorAutoConfiguration,\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusLanguageDriverAutoConfiguration,\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
那么直接看最后一个:
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
看下源码:
@Configuration(proxyBeanMethods = false)
// 导入mybatis就有这两个类
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
// 如果只有一个数据库连接池的类
@ConditionalOnSingleCandidate(DataSource.class)
// 配置文件中和类属性进行绑定
@EnableConfigurationProperties(MybatisPlusProperties.class)
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {
继续看看类中配置了什么:
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
这段代码就是配置SqlSessionFactory,然后将容器中的DataSource导入进来,然后下面在进行疯狂的set操作。
SqlSessionTemplate的配置,也就是SqlSession的配置
@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);
}
}
AutoConfiguredMapperScannerRegistrar的注册,也就是配置的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.");
}
}
看一下配置文件:
/**
* Configuration properties for MyBatis.
*
* @author Eddú Meléndez
* @author Kazuki Shimizu
*/
@Data
@Accessors(chain = true)
@ConfigurationProperties(prefix = Constants.MYBATIS_PLUS)
public class MybatisPlusProperties {
private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
/**
* Location of MyBatis xml config file. 全局配置文件
*/
private String configLocation;
/**
* Locations of MyBatis mapper files.
* mapper映射接口的文件的位置,这里默认的是/mapper路径下的所有的xml文件
* @since 3.1.2 add default value
*/
private String[] mapperLocations = new String[]{"classpath*:/mapper/**/*.xml"};
/**
* Packages to search type aliases. (Package delimiters are ",; \t\n") 别名操作
*/
private String typeAliasesPackage;
/**
* The super class for filtering type alias.
* If this not specifies, the MyBatis deal as type alias all classes that searched from typeAliasesPackage.
*/
private Class<?> typeAliasesSuperType;
/**
* Packages to search for type handlers. (Package delimiters are ",; \t\n")
*/
private String typeHandlersPackage;
/**
* Indicates whether perform presence check of the MyBatis xml config file. 是否需要来进行检查全局配置文件
*/
private boolean checkConfigLocation = false;
/**
* Execution mode for {@link org.mybatis.spring.SqlSessionTemplate}.
*/
private ExecutorType executorType;
/**
* The default scripting language driver class. (Available when use together with mybatis-spring 2.0.2+)
* <p>
* 如果设置了这个,你会至少失去几乎所有 mp 提供的功能
*/
private Class<? extends LanguageDriver> defaultScriptingLanguageDriver;
/**
* Externalized properties for MyBatis configuration.
*/
private Properties configurationProperties;
/**
* A Configuration object for customize default settings. If {@link #configLocation}
* is specified, this property is not used.
* TODO 使用 MybatisConfiguration mybatis-plus的全局配置
*/
@NestedConfigurationProperty
private MybatisConfiguration configuration;
/**
* TODO 枚举包扫描
*/
private String typeEnumsPackage;
配置文件
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mp?serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456
# mybatisplus几乎零配置
mybatis-plus:
configuration:
# 添加控制台日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3、配置逻辑删除
首先需要在yaml中来进行指定:
mybatis-plus:
global-config:
db-config:
# 要被删除的实体类中的字段
logic-delete-field: deleted
# 定义数据库中逻辑删除的数据的值
logic-delete-value: 1
# 定义数据库中未删除的值
logic-not-delete-value: 0
看一个测试:
@Test
public void testFindAll(){
List<User2> users = user2Mapper.selectList(null);
users.forEach(System.out::println);
}
查看对应的sql:
SELECT id,name,age,email,manager_id,create_time,update_time,version,deleted FROM user2 WHERE deleted=0
可以发现,这里已经自动的给我们添加上了对应的逻辑删除的值,而且对应着配置文件中的,只是会去查询未被删除的值,而不会再次去进行查询没有删除的值。
查看上面的SQL语句,我们可以看到,在进行select的字段里面有一个字段deleted,但是这个字段通常来说,是不需要出现在select后面的字段中的,因为不需要。
所以如果有需要,那么在实体类上加上注解:
@TableField(select = false)
private Integer deleted;
那么再次执行之后,就看不到这个值了。
看一下对应的SQL语句:
SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE deleted=0
可以看到这里已经没有了对应的deleted字段了。
在全局配置文件中,配置上了逻辑删除的字段的值。对于多个表来说,我们会将重复的字段设置成一样的。
但是像这种需要来进行单独配置的来说,那么就需要加上@TableField注解即可。
再来看一个测试:
@Test
public void testDeleteById(){
int i = user2Mapper.deleteById(1);
System.out.println(i);
}
然后查看对应的SQL语句:
UPDATE user2 SET deleted=1 WHERE id=1 AND deleted=0
可以看到这里做的是update语句,而不是delete语句。因为这里将deleted修改成了我们需要去进行修改的值了。
总结
配置了逻辑删除之后带来的影响:
逻辑删除 | select | insert | update | delete |
---|---|---|---|---|
无影响 | 追加where条件,过滤掉已经删除的数据 | 追加where条件,过滤掉已经删除的字段 | 转换成update语句 | |
注意,上述的影响,只针对mp自动注入的SQL生效。 如果是自己手动添加的自定义SQL,则不会生效。比如:
public interface User2Mapper extends BaseMapper<User2> {
@Select("select * from user2")
List<User2> selectRaw();
}
调用这个selectRaw
,则mp的逻辑删除不会生效。
逻辑删除可在application.yml
中进行全局配置,也可在实体类中用@TableLogic
进行局部配置。
4、自动填充
表中常常会有“新增时间”,“修改时间”,“操作人” 等字段。比较原始的方式,是每次插入或更新时,手动进行设置。mp可以通过配置,对某些字段进行自动填充,食用示例如下
- 在实体类中的某些字段上,通过
@TableField
设置自动填充;
@Data
public class User2 {
private Long id;
private String name;
private Integer age;
private String email;
private Long managerId;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
private Integer version;
@TableField(select = false)
private Integer deleted;
}
2、然后再添一个组件到容器中去
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
// 注意第二个参数要填写实体类中的字段名称,而不是表的列名称
@Override
public void insertFill(MetaObject metaObject) {
strictFillStrategy(metaObject,"createTime", LocalDateTime::now);
}
// 注意第二个参数要填写实体类中的字段名称,而不是表的列名称
@Override
public void updateFill(MetaObject metaObject) {
strictFillStrategy(metaObject,"updateTime", LocalDateTime::now);
}
}
然后来进行测试:
@Test
public void testInsert() {
User2 user = new User2();
user.setId(8L);
user.setName("王一蛋");
user.setAge(29);
user.setEmail("yd@baomidou.com");
user.setManagerId(2L);
user2Mapper.insert(user);
}
可以看到数据库中插入时间进行了更新
然后测试一下修改的:
@Test
public void testUpdate() {
User2 user = new User2();
user.setId(8L);
user.setName("王一蛋");
user.setAge(30);
user.setEmail("yd@baomidou.com");
user.setManagerId(2L);
user2Mapper.updateById(user);
}
查看一下数据库,发现对应的事件也是修改成功的
注意,自动填充仅在该字段为空时会生效,若该字段不为空,则直接使用已有的值
5、乐观锁插件
当出现并发操作时,需要确保各个用户对数据的操作不产生冲突,此时需要一种并发控制手段。悲观锁的方法是,在对数据库的一条记录进行修改时,先直接加锁(数据库的锁机制),锁定这条数据,然后再进行操作;而乐观锁,正如其名,它先假设不存在冲突情况,而在实际进行数据操作时,再检查是否冲突。乐观锁的一种通常实现是版本号 ,在MySQL中也有名为MVCC的基于版本号的并发事务控制。
在读多写少的场景下,乐观锁比较适用,能够减少加锁操作导致的性能开销,提高系统吞吐量。
在写多读少的场景下,悲观锁比较使用,否则会因为乐观锁不断失败重试,反而导致性能下降。
乐观锁的实现如下:
- 取出记录时,获取当前version
- 更新时,带上这个version
- 执行更新时, set version = newVersion where version = oldVersion
- 如果oldVersion与数据库中的version不一致,就更新失败
这种思想和CAS(Compare And Swap)非常相似。
乐观锁的实现步骤如下:
配置乐观锁插件
@Configuration
public class MybatisPlusConfig {
/**
* 3.4.0以后的mp版本,推荐用如下的配置方式
* @return
*/
/* @Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}*/
/**
* 旧版mp可以采用如下方式。注意新旧版本中,新版的类,名称带有Inner,旧版的不带,不要配错了
* @return
*/
@Bean
public OptimisticLockerInterceptor opLocker() {
return new OptimisticLockerInterceptor();
}
}
写一个单元测试:
@Test
public void testOpLocker() {
int version = 1; // 假设这个version是先前查询时获得的
User2 user = new User2();
user.setId(8L);
user.setEmail("version@baomidou.com");
user.setVersion(version);
int i = user2Mapper.updateById(user);
}
看一下对应的SQL语句:
UPDATE user2 SET email='version@baomidou.com', update_time='2021-11-05T00:34:08.190', version=2 WHERE id=8 AND version=1 AND deleted=0
可以看到,在插入的时候首先来进行查询对应的version的值,如果是符合的,那么就开始进行操作。
当UPDATE返回了1,表示影响行数为1,则更新成功。反之,由于WHERE后面的version与数据库中的不一致,匹配不到任何记录,则影响行数为0,表示更新失败。更新成功后,新的version会被封装回实体对象中。
注意,乐观锁插件仅支持updateById(id)
与update(entity, wrapper)
方法
注意:如果使用wrapper
,则wrapper
不能复用! 示例如下
@Test
public void testOpLockerTwo() {
User2 user = new User2();
user.setId(8L);
user.setVersion(1);
user.setAge(2);
// 第一次使用
LambdaQueryWrapper<User2> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User2::getName, "王一蛋");
user2Mapper.update(user, wrapper);
// 第二次复用
user.setAge(3);
user2Mapper.update(user, wrapper);
}
查看下对应的SQL如下所示:
Consume Time:20 ms 2021-11-05 00:38:48
Execute SQL:UPDATE user2 SET age=2, update_time='2021-11-05T00:38:47.079', version=2 WHERE deleted=0 AND (name = '王一蛋' AND version = 1)
Consume Time:19 ms 2021-11-05 00:38:48
Execute SQL:UPDATE user2 SET age=3, update_time='2021-11-05T00:38:47.079', version=3 WHERE deleted=0 AND (name = '王一蛋' AND version = 1 AND version = 2)
可以看到在第二次复用wrapper
时,拼接出的SQL中,后面WHERE语句中出现了2次version,是有问题的。
6、性能分析插件
该插件会输出SQL语句的执行时间,以便做SQL语句的性能分析和调优。
注:3.2.0版本之后,mp自带的性能分析插件被官方移除了,而推荐使用第三方性能分析插件
1、引入maven依赖
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.9.1</version>
</dependency>
2、修改application.yaml
spring:
datasource:
driver-class-name: com.p6spy.engine.spy.P6SpyDriver #换成p6spy的驱动
url: jdbc:p6spy:mysql://localhost:3306/mp?serverTimezone=Asia/Shanghai #url修改
username: root
password: root
3、在src/main/resources
资源目录下添加spy.properties
#spy.properties
#3.2.1以上使用
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
# 真实JDBC driver , 多个以逗号分割,默认为空。由于上面设置了modulelist, 这里可以不用设置driverlist
#driverlist=com.mysql.cj.jdbc.Driver
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
#若要日志输出到文件, 把上面的appnder注释掉, 或者采用下面的appender, 再添加logfile配置
#不配置appender时, 默认是往文件进行输出的
#appender=com.p6spy.engine.spy.appender.FileLogger
#logfile=log.log
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
# 执行时间设置, 只有超过这个执行时间的才进行记录, 默认值0, 单位毫秒
executionThreshold=10
随便执行一条SQL语句,可以看到控制台会打印出来对应的日志:
Consume Time:19 ms 2021-11-05 00:38:48
Execute SQL:UPDATE user2 SET age=3, update_time='2021-11-05T00:38:47.079', version=3 WHERE deleted=0 AND (name = '王一蛋' AND version = 1 AND version = 2)
8、代码生成器
详细的去官网或者博客找一篇看看就行了,这里不再来进行赘述。
7、快速开始操作
mybatis-plus提供了两个接口,BaseMapper,也就是mapper接口;另外一个IService,则是service接口
二者最大的区别在于service中支持了大量的批量操作。
mp最强大的一点在于提供了wapper,可以非常的构造出来where条件。从这里可以看到这里是拼接where条件的。
那么只要知道了这个宗旨,那么后面的比较好理解了。
AbstractWrapper接口本身已经提供了很多的方法用于构建where条件。
而QueryWrapper接口主要用作查询;
UpdateWrapper接口用来进行set方法,用于进行set值的;
这块官方文档说明的还算详细。
在条件构造的过程中,有存在着一个boolean类型的condition参数,用来决定最终的条件是否需要添加到where条件中去。
lambda条件构造器,支持lambda表达式。
接下来就没有什么好说的了,具体的就可以来参考文档操作了。
参考文档:https://mp.weixin.qq.com/s/SBkYZrBbGEgBe09erNr7tg