MyBatis系列(八)——MyBatis-Plus的使用

前言

在之前的学习中我们已经知道了有关MyBatis的基本使用,也讲解了MyBatis Generator作为拓展插件对我们持久层开发的简化步骤,在本篇文章中,我们将讲解现阶段使用更多、功能更加强大的MyBatis增强工具MyBatis-Plus的使用方式和特性,希望对各位读者有所帮助。

想要了解更多MyBaits系列文章,可以从下面的传送门阅读:

MyBatis系列(一)——MyBatis的介绍和CRUD
MyBatis系列(二)——MyBatis的动态代理和映射文件动态配置
MyBatis系列(三)——MyBatis的类型处理器和PageHelper分页插件
MyBatis系列(四 )——MyBatis的多表操作
MyBatis系列(五)——Spring整合MyBatis
MyBatis系列(六)——MyBatis的注解开发
MyBatis系列(七)——逆向工程


为什么在讲了逆向工程之后,还要再讲MyBatis-Plus的使用呢?

主要有两点,从功能上来看逆向工程确实不如后起之秀MyBatis-Plus这般强大,除了简化代码开发外,还提供内置分页插件提供主键生成策略sql性能分析插件等多种功能。另外,从整合性来看,MyBatis-Plus是MyBaits的增强,我们可以直接使用MyBatis-Plus的依赖就可以完成对应的持久层操作,而无需再引入MyBatis。也不用像逆向工程一般引入插件来生成代码。
客观上来说,MyBatis-Plus确实是一款能够较好满足开发需求的一款优秀工具。也是值得后端程序员学习和掌握的技术。好了,接下来就开始我们的应用吧。

一、MyBatis-Plus的简单使用

由于MyBatis-Plus和springboot整合比较方便,下面的话我们就用springboot框架来进行演示。快速开始很简单,我们可以跟着官网的入门快速过一遍。

1. 初始化测试数据

新建一个空的数据库,然后创建数据表并插入测试数据

DROP TABLE IF EXISTS user;

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

DELETE FROM user;

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

初始化项目需要的依赖不多,主要是数据库驱动、web模块和Lombok插件的依赖

初始化springboot项目

初始化完成后,我们再引入MyBatis-Plus的依赖

        
            com.baomidou
            mybatis-plus-boot-starter
            3.0.5
        
3. 配置application.properties

配置springboot启动必要参数

# 配置springboot启动端口
server.port=9988
# 配置数据库参数
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis-plus
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
4. 配置Mapper接口

需要注意的是,Mapper接口继承了BaseMapper,这也是Mapper功能强大的原因之一——继承了父类已经编写好的通用方法。

public interface UserMapper extends BaseMapper {

}
5. 配置POJO类
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}
6. 在主启动类上配置包扫描MapperScan("com.qiqv.mybatisplus.mapper")
@SpringBootApplication
@MapperScan("com.qiqv.mybatisplus.mapper")
public class MybatisPlusApplication {

    public static void main(String[] args) {
        SpringApplication.run(MybatisPlusApplication.class, args);
    }

}
7. 创建测试模块中进行测试
@SpringBootTest
class MybatisPlusApplicationTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    void contextLoads() {
        userMapper.selectList(null).forEach(System.out::println);
    }
}
8. 运行测试用例
运行测试用例
用例演示结果

从上面我们可以看到,出去数据库的初始化之外,我们在项目中需要配置的只有接口和pojo类,不再需要配置映射文件,不需要映射文件,绝大部分通用方法都可以直接开箱即用!这就是MyBatis-Plus的方便所在!

二、日志配置

我们可以通过配置来显示持久层具体执行的sql语句

# 配置显示sql,显示格式为标准输出
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

三、常规的CRUD操作

我们在创建Mapper接口时,已经继承了BaseMapper接口,所以MyBatis-Plus为我们动态生成实现类的时候,我们的mapper类就可以使用我们的继承的方法进行使用。相关的API基本上都是见名知义,很好理解。

@SpringBootTest
class MybatisPlusApplicationTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    void contextLoads() {
        userMapper.selectList(null).forEach(System.out::println);
    }
    @Test
    void insertTest(){
        User user = new User(null, "xiaoming", 15, "[email protected]");
        int result = userMapper.insert(user);
        System.out.println("插入结果为"+ (result>0?"成功":"失败"));
    }

    // 注意,这里的updateById只会根据对象中已有的属性进行更新
    // 比如下面的user参数age为null,这并不会更新到数据表中
    @Test
    void updateTest(){
        User user = new User(5l,"xiaohong",null,null);
        int result = userMapper.updateById(user);
        System.out.println("更新结果为"+ (result>0?"成功":"失败"));
    }

    @Test
    void deleteTest(){
        int result = userMapper.deleteById(5l);
        System.out.println("删除结果为"+ (result>0?"成功":"失败"));
    }
}

三、MyBatis-Plus的主键生成策略

我们在不传递主键id值的情况下插入一条数据会发现,这条数据的id是一串长长的数字,那么这串数字是怎么生成的呢?这就要涉及到MyBatis的主键生成策略了。


雪花算法生成主键id

在默认情况下,如果我们不对对象中的主键ID进行设置的话,那么MyBatis-Plus会帮我们使用雪花算法来生成对应的ID。

那什么是雪花算法呢?

雪花算法(snowflake)是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。可以保证几乎全球唯一!


那我们可以根据需要来定义ID的生成策略吗?

答案是自然可以。我们只需要在实体类的主键字段写上对应的主键生成策略注解即可

那么注解的策略都有哪些呢?我们可以看一下下面这幅图

主键生成策略注解

下面我们来将生成策略改为AUTO

步骤一:在实体类的主键上加入注解@TableId
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;
}
步骤二:将数据库表ID设置为自增

注意,如果少了这一步的话插入数据的时候会报错。


设置表主键ID自增

使用自增长策略后,我们可以发现,现在新增的数据已经根据我们定义的增长策略生成ID了


自增长策略演示结果

四、自动填充功能

在开发项目的时候,比较常见的场景是:我们会给表设置create_timeupdate_time等字段,但这些字段我们并不想要每次插入或者更新数据的时候手动输入值来更新,有没有什么解决办法呢?
这个自然是有的,我们可以使用数据库客户端工具或者是使用SQL语句来设置字段的默认值。从数据库的角度出发自然可以,但是对于一些比较大型的项目我们一般是没有权限直接操作数据库的。这个时候我们就可以考虑在代码的角度的解决这个问题。

MyBatis-Plus为我们提供了@TableField注解来帮助我们解决这个问题,我们可以设置在什么时候触发这个自动填充机制。我们可以看到,MyBatis-Plus一共提供了四种填充时机的策略给我们,基本上见名知义这里我就不再解释了。

public enum FieldFill {
    /**
     * 默认不处理
     */
    DEFAULT,
    /**
     * 插入填充字段
     */
    INSERT,
    /**
     * 更新填充字段
     */
    UPDATE,
    /**
     * 插入和更新填充字段
     */
    INSERT_UPDATE
}

下面我们来演示一下使用MyBatis-Plus进行自动填充的步骤:

步骤一:给表新增create_timeupdate_time两个字段
ALTER TABLE user ADD create_time datetime; 
ALTER TABLE user ADD update_time datetime;
步骤二:给实体类加入对应的注解
public class User {
    ...
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;
}
步骤三:创建自定义处理器

这一步的话需要注意两点:自定义的处理器需要实现MetaObjectHandler接口,另外我们需要使用@Component注解来注入我们的处理器。

@Component // 交由spring托管
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("fill data start...");
        this.setFieldValByName("createTime",new Date(),metaObject);
        this.setFieldValByName("updateTime",new Date(),metaObject);
        log.info("fill data end...");
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("fill data start...");
        this.setFieldValByName("updateTime",new Date(),metaObject);
        log.info("fill data end...");
    }
}
步骤四:在测试类中进行测试:
    @Test
    void insertTest(){
        User user = new User(null, "xiaoming", 12, "[email protected]",null,null);
        int result = userMapper.insert(user);
        System.out.println("插入结果为"+ (result>0?"成功":"失败"));
    }

    // 注意,这里的updateById只会根据对象中已有的属性进行更新
    // 比如下面的user参数age为null,这并不会更新到数据表中
    @Test
    void updateTest(){
        User user = new User(1379702643906240515l,"xiaohong",45,null,null,null);
        int result = userMapper.updateById(user);
        System.out.println("更新结果为"+ (result>0?"成功":"失败"));
    }
步骤五:验证结果是否正确
验证结果

五、乐观锁

我们可以使用MyBatis-Plus来实现乐观锁的效果。
先说说什么是乐观锁?
乐观锁其实并不是某一种特定的技术,而是某一种解决高并发的解决思路。

当要更新一条记录的时候,希望这条记录没有被别人更新
乐观锁实现方式:

  • 取出记录时,获取当前version
  • 更新时,带上这个version
  • 执行更新时, set version = newVersion where version = oldVersion
  • 如果version不对,就更新失败

接下来我们来介绍如何使用MyBatis-Plus来实现乐观锁:

步骤一:给表中新增版本字段
ALTER TABLE user ADD version INT DEFAULT 1 
步骤二:实体类加上新的字段和@Vesion注解
public class User {
    ...
    @Version
    private Integer version;
}
步骤三:注册乐观锁插件OptimisticLockerInterceptor
@Configuration
@EnableTransactionManagement
@MapperScan("com.qiqv.mybatisplus.mapper") // 由专门的配置类来扫描mapper接口
public class MyBatisPlusConfig {

    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor(){
        return new OptimisticLockerInterceptor();
    }
}
步骤四:模拟多线程,看乐观锁是否生效
    @Test
    void doubleThreadTest(){
        User user1 = userMapper.selectById(1l);
        user1.setName("xiaohei");
        User user2 = userMapper.selectById(1l);
        user2.setName("xiaozi");
        userMapper.updateById(user2);
        userMapper.updateById(user1);
    }

我们可以看控制台输出的结果,在我们使用乐观锁插件后,执行更新语句时MyBatis-Plus会帮助我们自动加上版本进行作为乐观锁判断的条件。从数据库的结果来看,我们的乐观锁也确确实实是生效了。

乐观锁sql代码

数据库结果演示

六、分页插件

我们在之前的文章中介绍了分页插件PageHelper,对于分页功能,其实MyBatis-Plus也有内置的分页插件供我们使用。我们来看看如何使用吧。

步骤一:注册分页插件
@Configuration
@EnableTransactionManagement
@MapperScan("com.qiqv.mybatisplus.mapper") // 由专门的配置类来扫描mapper接口
public class MyBatisPlusConfig {
    ...
    //注册分页插件
    @Bean
    public PaginationInterceptor paginationInterceptor(){
        return new PaginationInterceptor();
    }
}
步骤二:使用Page对象来获取分页结果
    @Test
    void pageTest(){
        // 第一个参数是pageIndex,第二个参数是pageSize
        Page userPage = new Page<>(2, 2);
        // 如果是条件查询,可以传入wrapper对象到第二个参数中
        userMapper.selectPage(userPage, null);
        userPage.getRecords().forEach(System.out::println);
        System.out.println("总个数:" + userPage.getTotal());
        System.out.println("当前页码" + userPage.getCurrent());
    }
}

就测试的结果来看,内置的分页插件确实可以帮助我们做分页的工作,但个人觉着分页的功能没有PageHelper那么齐全,类似于获取上一页、下一页这些参数的api是没有的。当然了,可能后续新的版本中有做改进。

八、逻辑删除

在我们开发中可能会遇到这样一种场景,用户选择删除某条记录时,此时我们并不会在数据库中对该条数据进行删除,而是通过变更数据的状态为删除的做法,来实现逻辑删除的效果。
下面我们就来演示一下吧:

步骤一:表中新增字段
ALTER TABLE user ADD deleted INT(1) DEFAULT 0
步骤二:实体类添加属性
public class User {
    ...
    @TableLogic
    private Integer deleted;
}
步骤三:注册逻辑删除插件
@Configuration
@EnableTransactionManagement
@MapperScan("com.qiqv.mybatisplus.mapper") // 由专门的配置类来扫描mapper接口
public class MyBatisPlusConfig {
    ...
    @Bean
    public ISqlInjector iSqlInjector(){
        return new LogicSqlInjector();
    }
}
步骤四:在application.properties文件中配置逻辑删除的默认值
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0
步骤五:在代码中进行测试
    @Test
    void logicDeleteTest(){
        userMapper.deleteById(1379702643906240513l);
    }

我们可以观察一下控制台输出的语句,逻辑删除插件已经帮我们动态做好了逻辑删除的sql拼接。


逻辑删除-控制台输出
步骤六:验证逻辑删除是否成功

我们可以看到,我们执行普通查询的时候,MyBatis-Plus插件还会帮我们添加上deleted=0的条件过滤掉已经被逻辑删除的数据。

验证逻辑删除-控制台输出

九、性能分析插件

MyBatis-Plus还为我们提供了sql的性能分析插件,帮助我们找到项目中的慢查询。使用方式也十分简单

步骤一:
public class MyBatisPlusConfig {
    ...
    // 性能分析插件
    @Bean
    @Profile({"dev","test"}) // 配置使用的环境,一般来说生产环境是不用这个插件的
    public PerformanceInterceptor performanceInterceptor(){
        PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
        // 设置最大执行时长
        performanceInterceptor.setMaxTime(1000);
        // 设置是否格式化代码
        performanceInterceptor.setFormat(true);
        return performanceInterceptor;
    }
}
步骤二:在application.properties文件中配置spring当前运行环境
# 配置spring当前环境
spring.profiles.active=dev
步骤三:在代码中进行测试
    @Test
    void contextLoads() {
        userMapper.selectList(null).forEach(System.out::println);
    }
性能分析-控制台输出

我们可以看到,现在我们的控制台会对底层执行的sql语句进行格式化显示,同时显示对应的执行时间。

十、条件构造器

对于一些复杂的查询条件,单纯地使用Mapper继承的方法可能会不够用,这时我们就可以使用 MyBatis-Plus的条件构造器来帮助我们实现这个功能。这里的条件构造器可以类比为使用逆向工程生成的Example -> criteria
实现的步骤其实并不复杂,创建QueryMapper对象后,把所需要的条件封装上去后,调用Mapper的相关方法即可。

    // 要求查询出所有名称后缀为hong,年龄大于15岁,更新时间字段不为空的所有数据
    @Test
    void multiplyConditionTest(){
        QueryWrapper wrapper = new QueryWrapper<>();
        wrapper.isNotNull("update_time")
                .gt("age",15)
                .likeLeft("name","hong");
        List users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

    // 要求查询出所有年龄在 16-25的用户
    @Test
    void betweenTest(){
        QueryWrapper wrapper = new QueryWrapper<>();
        wrapper.between("age",16,25);
        userMapper.selectList(wrapper).forEach(System.out::println);
    }

十一、代码自动生成器

MyBatis-Plus也拥有和逆向工程相似的代码自动生成功能,而且它还能完成controllerservice层代码的生成,比逆向工程更为强大。我们下面就来看一下如何使用吧。
我们新建一个springboot项目,除了原有的依赖外导入代码生成引擎依赖和swagger依赖:


    ...
        
            org.apache.velocity
            velocity-engine-core
            2.3
        
        
            io.springfox
            springfox-swagger2
            2.6.1
        
    

新建一个类作为代码生成类

public class MPGenertorBean {
    public static void main(String[] args) {
        // 构建一个代码生成器对象
        AutoGenerator autoGenerator = new AutoGenerator();

        // 配置全局策略
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/mybatis-plus-generator/src/main/java");
        gc.setAuthor("moutory"); //配置作者信息
        gc.setOpen(false); // 是否打开输出目录
        gc.setFileOverride(false); // 是否覆盖
        gc.setServiceName("%sService"); // 去Service的I前缀
        gc.setIdType(IdType.ID_WORKER); //设置id自增策略
        gc.setDateType(DateType.ONLY_DATE);
        gc.setSwagger2(true); //是否生成swagger
        autoGenerator.setGlobalConfig(gc);

        // 配置数据源
        DataSourceConfig dsConfig = new DataSourceConfig();
        dsConfig.setUrl("jdbc:mysql://localhost:3306/mybatis-plus");
        dsConfig.setUsername("root");
        dsConfig.setPassword("root");
        dsConfig.setDriverName("com.mysql.jdbc.Driver");
        dsConfig.setDbType(DbType.MYSQL);
        autoGenerator.setDataSource(dsConfig);

        //配置包
        PackageConfig pc = new PackageConfig();
        pc.setModuleName("demo");
        pc.setParent("com.qiqv");
        pc.setController("controller");
        pc.setMapper("mapper");
        pc.setEntity("pojo");
        pc.setService("service");
        autoGenerator.setPackageInfo(pc);

        // 配置策略
        StrategyConfig strategyConfig = new StrategyConfig();
        strategyConfig.setInclude("user"); // 设置要映射的表名,可以一次传多个
        strategyConfig.setNaming(NamingStrategy.underline_to_camel); //数据库表映射到实体的命名策略:下划线转驼峰命名
        strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略, 未指定按照 naming 执行
        strategyConfig.setEntityLombokModel(true); // 自动Lombok
        strategyConfig.setLogicDeleteFieldName("deleted");
            //自动填充设置
        TableFill gmtCreate = new TableFill("create_time", FieldFill.INSERT);
        TableFill gmtModified = new TableFill("update_time",FieldFill.INSERT_UPDATE);
        ArrayList tableFills = new ArrayList<>();
        tableFills.add(gmtCreate);
        tableFills.add(gmtModified);
        strategyConfig.setTableFillList(tableFills);
            //配置乐观锁
        strategyConfig.setVersionFieldName("version");
        strategyConfig.setRestControllerStyle(true);
        strategyConfig.setControllerMappingHyphenStyle(true);//指定rest请求风格
        autoGenerator.setStrategy(strategyConfig);
        autoGenerator.execute();

    }
}

我们跑一下程序就可以发现,对应模块的代码已经出来了


自动生成代码结果图

至此,对于MyBatis-Plus的使用就介绍到这里了,相关的源码可以在我的码云上面下载
除了自动生成代码外的所有案例代码:https://gitee.com/moutory/mybatis-plus-demo
自动生成代码案例:https://gitee.com/moutory/mybatis-plus-generator

说在最后

其实本篇文章是打算跟逆向工程的文章合在一起发的,但觉得Mybatis-Plus的内容点比较多,一起讲的话可能比较杂乱,所以就还是单独分出了一篇文章出来。基本上本篇文章中和MyBaits-Plus的知识点相关的内容,都可以从它的官网上找的到,也强烈推荐大家去官网上查看它的更多实用功能和新特性。
需要注意的是,文章中使用的MyBatis-Plus版本是3.0.5,其实是比较老了,现在的版本是3.4.x了,所以文章中的部分API和最新版本的可能略有不同。还是很建议大家在实际学习中,尽可能地去官方上面看对应的文档,毕竟是中文写的,阅读起来并不难。

资料参考:
狂神说 / Bilibili狂神说Java笔记:https://gitee.com/kuangstudy/openclass

(一个蛮好的up主,大家也可以关注一下)

你可能感兴趣的:(MyBatis系列(八)——MyBatis-Plus的使用)