MybatisPlus02_IService和各种插件

发布时间 2023-06-03 15:25:00作者: Purearc

前言

上次 忘了把 application.yml 放出来,以至于没有配置日志,log-impl 后面的值表示输出日志到控制台。

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto
  mapper-locations: classpath:mybatis/*Mapper.xml

image-20230531195133768

一、IService

​ 就像 BaseMapper 一样,Mybatis-Plus 提供了 IService 接口封装了常用的 Service 层操作,同样自定义 Mapper 要继承 BaseMapper;Service 继承 IService ,实现类实现 Service 并且继承 ServiceImpl ,ServiceImpl 里面的泛型为 自定义 Mapper 和对应的 POJO。

package com.purearc.plus2.mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

package com.purearc.plus2.service;
public interface UserService extends IService<User> {
}

package com.purearc.plus2.service.imlp;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService   {
}

​ 可以看到 IService 封装了很多常用的 Service 层操作。

image-20230601095257488

    @Autowired
    private UserService userService;

    @RequestMapping("/count")
    public Map<String,Object> getUserCount(){
        Map result = new HashMap();
        int count = userService.count();
        result.put("count",count);
        return result;
    }

​ 自己的 UserService 中也是什么都没写。

image-20230601100700510

​ 从日志中可以看到,SQL 执行的是 count 操作。

image-20230601100800078

​ 在 IService 中同样封装了逻辑删除的 move 方法,实际上是一个 update ,即改变字段 is_delete 的值。

image-20230601102305435

image-20230601102525478

二、条件查询

​ queryWrapper 是 mybatis-plus 中实现查询的对象封装操作类(条件构造器),可以封装sql对象,包括where条件,order by排序,select哪些字段等等,我理解的就是给查询的操作加上附加条件的。

image-20230601110726284

类 / 接口 作用
Wrapper 条件构造抽象类,最顶端父类
AbstractWrapper 用于查询条件封装,生成sql的where条件
AbstractLambdaWrapper Lambda语法使用 Wrapper 统一处理解析lambda获取column。
LambdaQueryWrapper 用于lambda语法使用的查询Wrapper
LambdaUpdateWrapper Lambda更新封装Wrapper
QueryWrapper Entity对象封装操作类,不是用lambda
UpdateWrapper Update条件封装,用于Entity对象更新操作

​ 在下面这段代码中,我们使用到 QueryWrapper 也是最常用的类:传入的是一个 User 对象的 JSON 格式描述,转换为 User 对象后解析传入的对象的各个属性:

​ 如果我们传入的 User 的某个属性值不是空,就对其进行包装操作 wrapper.eq("id",user.getId());表示我们传入的 id .equals("数据表中的 id"),后面的 like 那必然是模糊查询,使用 Wrapper 工具类相当于完成了在 mybatis 的 xml 配置里面的 动态 SQL

    @RequestMapping("/list")
    public List<User> list(@RequestBody User user){
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        if(!ObjectUtils.isEmpty(user.getId())){
            wrapper.eq("id",user.getId());
        }
        if(!ObjectUtils.isEmpty(user.getName())){
            wrapper.like("name",user.getName());
        } if(!ObjectUtils.isEmpty(user.getEmail())){
            wrapper.like("email",user.getEmail());
        }
        wrapper.orderByDesc("id");
        return userService.list(wrapper);
    }

​ 下图的结果上部分表示只传入一个 id,下面表示传入 id 和 name。

image-20230601143921626

三、Mybatis-Plus 分页

​ 在传统的 MyBatis 应用中,我们使用 PageHelper 或者自己实现分页。但是在 MyBatis Plus 中,你可以通过 MybatisPlusInterceptor 快速实现分页。MybatisPlusInterceptor是一系列的实现 InnerInterceptor 的拦截器链,也可以理解为一个集合。可以包括如下的一些拦截器:

自动分页: PaginationInnerInterceptor(最常用)

多租户: TenantLineInnerInterceptor

动态表名: DynamicTableNameInnerInterceptor

乐观锁: OptimisticLockerInnerInterceptor

sql性能规范: IllegalSQLInnerInterceptor

防止全表更新与删除: BlockAttackInnerInterceptor

​ 如果你发现有个报错:Cannot resolve symbol ‘MybatisPlusInterceptor‘ ,版本问题,3.4.0 之后才有这个插件,改一下就行了。

​ 在项目下新建一个软件包专门存放配置类, 使用 @MapperScan 表示要进行分页的 Mapper 是哪个;然后需要创建一个 MybatisPlusInterceptor 对象用来配置 MP 插件,那我们具体要配置什么插件就需要在插件对象中再添加一个内部插件,使用interceptor.addInnerInterceptor()方法,方法参数为要使用的插件,当前所使用的是 PaginationInnerInterceptor(DbType.MYSQL));

package com.purearc.plus2.config;
@Configuration
@MapperScan("com.purearc.plus2.mapper")
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}

​ 在 Controller 中,创建了一个 Page 对象,该对象有两个 Interger 参数,第一个是 当前的页码,第二个是 分页的数量;创建好对象之后调用从 IService 继承过来的 page 方法,可以只传入 Page 对象,也可以把条件构造器一起传进去为 SQL 语句添加查询条件。

    @RequestMapping("/listPage")
    public Page listPage(@RequestBody User user){
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        if(!ObjectUtils.isEmpty(user.getId())){
            wrapper.eq("id",user.getId());
        }
        if(!ObjectUtils.isEmpty(user.getName())){
            wrapper.like("name",user.getName());
        } if(!ObjectUtils.isEmpty(user.getEmail())){
            wrapper.like("email",user.getEmail());
        }
        wrapper.orderByDesc("id");

        //设置分页的参数  1:当前的页码  4:当前页的数量
        Page<User> page = new Page<>(2,4);
        userService.page(page,wrapper);
        return page;
    }
}

​ 我们给 Page 传入的是 (2,4)也就是显示第二页,一共分为页,所以第二页的 Index 就是从 4 开始的;同时我们可以看到 Preparing 后的 SQL 语句加上了 LIMITE ?,?

image-20230601171825943

四、Mybatis-Plus 多数据源

(一)配置数据源

​ 既然是多数据源那肯定就得有多个数据库,在这里创建了一个叫 mybatis2 的数据库,并且在里面创建了这么一张表。

CREATE TABLE `address_info` (
  `id` bigint NOT NULL,
  `address` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `user_id` bigint DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

​ 这个表就用来存储地址的信息(假设人和住址都是唯一的,当然实际不可能),user_id 是从 mybatis 数据库的 user 表复制出来的。

image-20230601180716213

​ 为了配置多数据源还需要引入这个依赖包,楼主引入的时候好几次没成功,后来换了网络环境就好了。

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.0</version>
</dependency>

​ 在 SpeingBoot 的核心配置文件中添加数据源的信息,这个 db1 和 db2 只是标记数据源的名字,可以随便改,另外可以设置默认的数据源,一般也会吧严格匹配关掉,匹配不到就用默认的数据源。

#连接数据库
spring:
  #设置连接的数据源
  datasource:
    dynamic:
      primary: db1 #设置默认的数据源或者数据源组
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        db1:
          url: jdbc:mysql://127.0.0.1:3306/mybatis?characterEncoding=UTF-8&useSSL=false&useUnicode=true&serverTimezone=UTC&allowPublicKeyRetrieval=true
          username: root
          password: a.miracle
          driver-class-name: com.mysql.cj.jdbc.Driver
        db2:
          url: jdbc:mysql://127.0.0.1:3306/mybatis?characterEncoding=UTF-8&useSSL=false&useUnicode=true&serverTimezone=UTC&allowPublicKeyRetrieval=true
          username: root
          password: a.miracle
          driver-class-name: com.mysql.cj.jdbc.Driver

(二)完善实体类

​ 对应 mybatis2 中数据表的 POJO

package com.purearc.plus2.model;
@Data
@Accessors(chain = true)
public class AddressInfo {
    private Long id;
    private String address;
    private Long userId;
}

(三)完善 Service 、Mapper、Controller

​ 在这里使用注解 @DS 来配置此服务的数据源,如果不适用这个注解就会使用我们在 yml 文件中配置的默认数据源。

package com.purearc.plus2.service;
public interface AddressInfoService extends IService<AddressInfo> {
}

package com.purearc.plus2.service.imlp;
@DS("db2")
@Service
public class AddressInfoServiceImpl extends ServiceImpl<AddressInfoMapper, AddressInfo> implements AddressInfoService {
}

​ mapper 还是那样,继承 BaseMapper。

package com.purearc.plus2.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.purearc.plus2.model.AddressInfo;

public interface AddressInfoMapper extends BaseMapper<AddressInfo> {
}

​ 我们先传入一个 User 的 id ,再加入条件 mybatis2 中的 address_info 中的 user_id 的值要等于传入的 User 的 id 值,这样就能找到这个 User 的 address 信息。

    @RequestMapping("/getById")
    public User getById(Long id){
        User user = userService.getById(id);
        if(user == null){
            return null;
        }
        QueryWrapper<AddressInfo> wrapper = new QueryWrapper<>();
        wrapper.eq("user_id",user.getId());
        AddressInfo addressInfo = addressInfoService.getOne(wrapper);
        System.out.println(addressInfo);
        return user;
    }

​ 我们可以看到,在整个运行过程中关闭了一次数据源,由新开了一个,说明确实在中途切换了数据源。

image-20230601190406267

五、使用 IEnum

​ MyBatisPlus解决了繁琐的配置,让 MyBatis 更优雅的适用枚举属性。从 3.5.2 版本开始只需完成 声明通用枚举属性 即可使用。

package com.purearc.plus2.enums;
public enum SexEnum {
    MALE(1,"男"),
    FALMALE(2,"女");

    @EnumValue
    private Integer sex;
    private String sexName;

    SexEnum(Integer sex, String sexName) {
        this.sex = sex;
        this.sexName = sexName;
    }
}

​ 为 User 实体类添加 SexEnum 属性,那自然在数据表中也添加了 sex 字段,下面的 @TableFiled 注解也能看出来;

package com.purearc.plus2.model;
@Data
@Accessors(chain = true)
@TableName("user")
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    @TableLogic(value = "0",delval = "1")
    private Integer isDelete;
    //使用 ENUM 表示性别
    @TableField(value = "sex")
    private SexEnum sexEnum;
}

​ 在 save2 中把 User 放进去之前又给 SexEnum(实际就是 sex)赋值为 SexEnum 枚举类中的 FAMLE ,存入数据表后我们不难发现对应的值变成了 2;但是在我们再次执行查询的时候查询出来的结果却是 FAMALE。

    @RequestMapping("/save2")
    public Map<String,Object> save2(@RequestBody User user){
        Map result = new HashMap();
        user.setSexEnum(SexEnum.FALMALE);
        boolean saveResult = userService.save(user);
        result.put("result",saveResult);
        return result;
    }

image-20230602163704204

image-20230602164048826

PS:都有常量了为何要有枚举类?

​ 我们在不引入 ENUM 的情况下使用 Integer (String 啥的肯定也可以)定义四个不同的季节,由于这个是 static 和 final 的,肯定是能通过类 OGNL 直接调用且不能被改变的。

public class Season {
    /**
     * 使用 Integer 来定义春夏秋冬(其他类型是自然可以)
     */
    public static final int SPRING = 1;
    public static final int SUMMER = 2;
    public static final int AUTUMN = 3;
    public static final int WINTER = 4;
    /**
     * 定义静态常量可以在其它类中直接通过类名 OGNL 语句调用
     * @param args
     */
    public static void main(String[] args) {
        System.out.println(Season.SPRING);
        System.out.println(Season.SUMMER);
        System.out.println(Season.AUTUMN);
        System.out.println(Season.WINTER);
    }

    

​ 如果我们根据 Season 的值来区分不同的逻辑,假设为 方法 fun(),参数为 int 类型,但是我们用的时候只要传入一个 INT 值就可以了,当然你也可以写上范围,或者进行逻辑判断,但是对于使用者就不是很友好。

	/**
     *
     * @param season
     * 只要传递int就可以
     */
//可能出现的情况

	fun(Season.Spring);
	fun(3);
    fun(4);
    fun(100);

    public static void fun(int season) {
        switch (season) {
            case 1:
                System.out.println(1);
                break;
            case 2:
                System.out.println(2);
                break;
            case 3:
                System.out.println(3);
                break;
            default:
                System.out.println(4);
        }
    }
}

​ 如果对于 Season 类,我们里面的成员都改成 Season 类型的,并且把类加上 final 修饰(防止继承),把构造器写成 private 的(防止外部实例化),然后在将 fun() 函数的参数改为 Season 类型的 ,这样就可以限制我在使用这个方法的时候只能传入 Season 类型,而且我这个类还是写死的,也不能 new 对象,我里面写了什么你就只能用什么,直接就在根源限制了你要传入的参数,况且 IDE 会给你提醒你要传入个什么东西。

public final class Season2 {
    /**
     * 保证季节类型只会有这些常量
     */
    public static final Season2 SPRING = new Season2();
    public static final Season2 SUMMER = new Season2();
    public static final Season2 AUTUMN = new Season2();
    public static final Season2 WINTER = new Season2();

    private Season(){}

    public static void fun2(Season2 season){
        //方法体
    }

    public static void main(String[] args) {
        fun2(Season2.AUTUMN);
        fun2("SPRING");
    }
}

​ 下面是改造过后的

image-20230602213305981

​ 这就是 ENUM 的设计原理,下面代码通过 javap 后得到的与我们的 Season2 基本一致,且 ENUM 本身就是一个类,所以限制了传入类型,但是我们只能通过 enum 关键字让我们的自定义枚举继承 ENUM 类,这是 java 的规范。

public enum SeasonEnum {
    SPRING,
    SUMMER,
    AUTUMN,
    WINTER
}

image-20230602213617568

六、代码生成器(新)

​ mybatis-plus 可以通过逆向工程根据数据表生成软件包和软件包中的类。

        <!--代码生成器(新)-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--模板引擎依赖-->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>

​ 使用代码生成器(新)版本至少为 3.5.1 ,写这篇博客的时候官网已经更新到 3.5.2,在这这里使用的 mybatis-plus 的多有版本都是 3.5.1。

package com.purearc.plus2.generator;
public class GeneratorTest {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8";
        String username = "root";
        String password = "";
        FastAutoGenerator.create(url, username, password)
                .globalConfig(builder -> {
                    builder.author("Purearc") // 设置作者
                            //.enableSwagger() // 开启 swagger 模式
                            .fileOverride() // 覆盖已生成文件
                            .outputDir("D://mybatisplus"); // 指定输出目录
                })
                .packageConfig(builder -> {
                    builder.parent("com.purearc") // 设置父包名
                            .moduleName("mybatisplus") // 设置父包模块名
                            .pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://mybatisplus")); // 设置mapperXml生成路径
                })
                .strategyConfig(builder -> {
                    builder.addInclude("money") // 设置需要生成的表名
                            .addTablePrefix("t_", "c_"); // 设置过滤表前缀
                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();
    }
}

​ 厉不厉害你 MP 哥。

image-20230603095822388

image-20230603100030591

七、乐观 / 悲观锁 、乐观锁插件

乐观锁悲观锁面试

(一)乐观锁和悲观锁

​ 乐观锁和悲观锁是两种用于并发场景数据竞争的思想。

​ 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。(CAS、版本号)

​ 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

(二)乐观锁插件

​ 乐观锁的实现有两种方式:1、CAS(Compare and Swap):使用预期值 V 和内存中的值 A,进行比较,如果相同就拟写入 B,否则不进行修改。2、版本号:定义一个 version,每当有修改时 version + 1,根据 version 判断数据是否被修改。

我们使用上面所提到的逆向工程生成 money 数据表所对应的软件包、类等。

image-20230603150051586

​ 将 OptimisticLockerInnerInterceptor 作为内部插件放入 MybatisPlusInterceptor 并返回。

package com.purearc.plus2.config;
@Configuration
@MapperScan("com.purearc.plus2.mapper")
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //分页
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        //乐观锁
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

​ 测试:获取到 moneyBean 后更改里面的 sum,再使用 update 将 moneyBean 作为参数传回去更新数据库。

@Controller
@RequestMapping("/mybatisplusGenerator/money")
public class MoneyController {
    @Autowired
    private IMoneyService moneyService;

    @RequestMapping("/changeMoney")
    public void changeMoney(int id, int money) {
        //查询到数据的时候version=1
        Money moneyBean = moneyService.getById(id);
        moneyBean.setSum(moneyBean.getSum()-money);
        //修改  修改的时候version=1
        moneyService.updateById(moneyBean);
    }
}

​ 在 Money 的 POJO 上对于 version 字段添加了 @Version 注解用于实现乐观锁

@Data
@Accessors(chain = true)
public class Money implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long id;
    private String personName;
    private int sum;
    @Version
    private int version;
}

​ version 的值增加。

​ 假设楼主要给张三转 1000 块钱,那首先就要获取他的当前余额(remain),这时候的 version 是 1;

​ 楼主转钱的事务开启,同时另一个线程 “张三花 200” 开启,将 version 变成 2;

​ 楼主转钱事务开始执行,系统发现版本号变了(被更改过了),事务取消执行,回滚到之前状态。

image-20230603150833618

image-20230603150859863