springboot-mybatisplus第一章节

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依赖:

springboot-mybatisplus第一章节

可以看到这里引入了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可以通过配置,对某些字段进行自动填充,食用示例如下

  1. 在实体类中的某些字段上,通过@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的基于版本号的并发事务控制。

在读多写少的场景下,乐观锁比较适用,能够减少加锁操作导致的性能开销,提高系统吞吐量。

在写多读少的场景下,悲观锁比较使用,否则会因为乐观锁不断失败重试,反而导致性能下降。

乐观锁的实现如下:

  1. 取出记录时,获取当前version
  2. 更新时,带上这个version
  3. 执行更新时, set version = newVersion where version = oldVersion
  4. 如果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条件的。

那么只要知道了这个宗旨,那么后面的比较好理解了。

springboot-mybatisplus第一章节

AbstractWrapper接口本身已经提供了很多的方法用于构建where条件。

而QueryWrapper接口主要用作查询;

UpdateWrapper接口用来进行set方法,用于进行set值的;

这块官方文档说明的还算详细。

在条件构造的过程中,有存在着一个boolean类型的condition参数,用来决定最终的条件是否需要添加到where条件中去。

lambda条件构造器,支持lambda表达式。

接下来就没有什么好说的了,具体的就可以来参考文档操作了。

参考文档:https://mp.weixin.qq.com/s/SBkYZrBbGEgBe09erNr7tg

上一篇:Mybatis-plus的分页查询SelectPage总是查询所有


下一篇:用了MybatisPlus后,我很久没有手写sql了