Mybatis-Plus(二)进阶篇

1,204 阅读7分钟

书接上文,讲解完MP的基本知识,我们已经可以独立完成增删改查的功能,本文将讲解一些MP更加深入的知识,让我们开始吧

主键策略

简单来说就是我们该用哪种方式生成主键,这里的主键策略和IdType相关,每一种IdType代表着一种主键生成策略

示例:@TableId(value = "id", type = IdType.INPUT)

全部的IdType如下:

描述备注
AUTO数据库 ID 自增AUTO自动增长策略,这个配合数据库使用,Mysql可以,但是Oracle不行
NONE无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)在 application.properties 中添加如下配置:mybatis-plus.global-config.db-config.id-type=auto
INPUTinsert 前自行 set 主键值INPUT进行自己传递主键即可,进行插入工作,但在插入之前一定要检查数据库是否已经存在了该主键Mybatis-Plus 内置了5个数据库主键序列(如果内置支持不满足你的需求,可实现 IKeyGenerator 接口来进行扩展,下面会详细讲解)
ASSIGN_ID分配 ID(主键类型为 Number(Long 和 Integer)或 String),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)使用雪花算法自动生成主键 ID(雪花算法自行了解)
ASSIGN_UUID分配 UUID,主键类型为 String,使用接口IdentifierGenerator的方法nextUUID(默认 default 方法)自动生成不含中划线的 UUID 作为主键

接下来我们分别测试一下吧

测试主键策略-AUTO

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Test
    public void testMPKeyGenerator() {
        User user = new User();
        user.setName("张三");
        user.setEmail("123@qq.com");
        user.setAge(12);
        user.setCreateTime(LocalDateTime.now());

        boolean save = userService.save(user);
        System.out.println(save);
    }

测试结果:主键ID+1

测试主键策略-NONE

application.properties

mybatis-plus:
	global-config:
		db-config:
			id-type: auto
    @TableId(value = "id", type = IdType.NONE)
    private Long id;

测试结果:主键ID+1

注意:注解里等于跟随全局,下面是TableId代码

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface TableId {
    String value() default "";

    IdType type() default IdType.NONE;
}

测试主键策略-INPUT

    @TableId(value = "id", type = IdType.INPUT)
    private Long id;
    @Test
    public void testMPKeyGenerator() {
        User user = new User();
        user.setId(100l);
        user.setName("张三");
        user.setEmail("123@qq.com");
        user.setAge(12);
        user.setCreateTime(LocalDateTime.now());

        boolean save = userService.save(user);
        System.out.println(save);
    }

注意:如果在input策略下不自己设置主键值,并且数据库主键不是自动增长就会报错


虽然MP内置了5种5个数据库主键序列:

  • DB2KeyGenerator
  • H2KeyGenerator
  • KingbaseKeyGenerator
  • OracleKeyGenerator
  • PostgreKeyGenerator

我们可以这样使用(由于作者只有MySQL数据库,所以这里代码参照官方):

实体类

@KeySequence(value = "SEQ_ORACLE_STRING_KEY", clazz = String.class)
public class YourEntity {

    @TableId(value = "ID_STR", type = IdType.INPUT)
    private String idStr;

}

配置类

@Configuration
public class MPConfig {
    @Bean
    public IKeyGenerator keyGenerator() {
        return new H2KeyGenerator();
    }
}

如果内置支持不满足你的需求,可实现 IKeyGenerator 接口来进行扩展

测试主键策略-ASSIGN_ID

    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private Long id;

测试结果:生成了 1577481548734758914 这样的主键值,并且雪花算法生成的位数在18-19位之间

测试主键策略-ASSIGN_UUID

    @TableId(value = "row_guid", type = IdType.ASSIGN_UUID)
    private String rowGuid;

测试结果:生成了 469870f6ff62a9608d7f5509031c6cec 这样的主键值

自定义ID生成器

MP自 3.3.0 开始,默认使用雪花算法 或者UUID(不含中划线)

ASSIGN_ID 使用接口 IdentifierGenerator 的方法 nextId (默认实现类为 DefaultIdentifierGenerator

    public Long nextId(Object entity) {
        return this.sequence.nextId();
    }

Sequence类的nextId

    public synchronized long nextId() {
        long timestamp = this.timeGen();
        if (timestamp < this.lastTimestamp) {
            long offset = this.lastTimestamp - timestamp;
            if (offset > 5L) {
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
            }

            try {
                this.wait(offset << 1);
                timestamp = this.timeGen();
                if (timestamp < this.lastTimestamp) {
                    throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                }
            } catch (Exception var6) {
                throw new RuntimeException(var6);
            }
        }

        if (this.lastTimestamp == timestamp) {
            this.sequence = this.sequence + 1L & 4095L;
            if (this.sequence == 0L) {
                timestamp = this.tilNextMillis(this.lastTimestamp);
            }
        } else {
            this.sequence = ThreadLocalRandom.current().nextLong(1L, 3L);
        }

        this.lastTimestamp = timestamp;
        return timestamp - 1288834974657L << 22 | this.datacenterId << 17 | this.workerId << 12 | this.sequence;
    }

而使用接口 IdentifierGenerator 的方法 nextUUID (默认 default 方法)

    default String nextUUID(Object entity) {
        return IdWorker.get32UUID();
    }

如果这些无法满足你的需求,我们可以实现 IdentifierGenerator

@Component
public class CustomIdGenerator implements IdentifierGenerator {

    @Override
    public Long nextId(Object entity) {
        //可以将当前传入的class全类名来作为bizKey,或者提取参数来生成bizKey进行分布式Id调用生成.
        String bizKey = entity.getClass().getName();
        System.out.println("===" + bizKey + "===");
        //自定义主键ID生成(这里使用hutool封装的雪花算法)
        Snowflake snowflake = IdUtil.getSnowflake(1, 1);
        return snowflake.nextId();
    }
}
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.2.5</version>
</dependency>

测试:控制台输出===mybatisplusdemo.User===,说明传入的类型是当前实体类类型,数据库插入的主键也是类似于 1577494790868701184 的一串数字

逻辑删除

什么是逻辑删除呢?之前也是没有接触过,删除就删除咋还有逻辑删除?接触了公司项目才知道什么是逻辑删除

平时自己在开发的时候可能会把无用的数据直接删除,用户删除数据那就是真在数据库删除,假如用户想找回数据基本不可能。而逻辑删除至少数据还在数据库里存在

其实逻辑删除也很简单就是增加一个字段来作为逻辑删除的标志位,一般都是1为删除 0为未删除。那肯定有人要问了那我们删除和查询的时候不还得自己去判断0,1么?当然不用,这些MP都帮我们做好了,只要我们设置好了,MP就会在删除和查询的时候帮我们自动加上过滤条件,就是这么方便

只对自动注入的 sql 起效:

  • 插入: 不作限制
  • 查找: 追加 where 条件过滤掉已删除数据,如果使用 wrapper.entity 生成的 where 条件也会自动追加该字段
  • 更新: 追加 where 条件防止更新到已删除数据,如果使用 wrapper.entity 生成的 where 条件也会自动追加该字段
  • 删除: 转变为 更新

例如:

  • 删除: update user set deleted=1 where id = 1 and deleted=0
  • 查找: select id,name,deleted from user where deleted=0

字段类型支持说明:

  • 支持所有数据类型(推荐使用 Integer,Boolean,LocalDateTime)
  • 如果数据库字段使用datetime,逻辑未删除值和已删除值支持配置为字符串null,另一个值支持配置为函数来获取值如now()

附录:

  • 逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。
  • 如果你需要频繁查出来看就不应使用逻辑删除,而是以一个状态去表示。

接下来我们写代码吧

我们需要在配置文件中增加逻辑删除的配置

mybatis-plus:
	global-config:
		db-config:
			logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
			logic-delete-value: 1 # 逻辑已删除值(默认为 1)
			logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

首先我们需要在原来的实体类上加个deleted字段,官方推荐 Integer,Boolean,LocalDateTime

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "user")
public class User {
    
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;


    @TableField(value = "name")
    private String name;

    @TableField(value = "age")
    private Integer age;

    @TableField(value = "email")
    private String email;

    @TableField(value = "create_time")
    private LocalDateTime createTime;

    @TableField(value = "deleted")
    private Integer deleted;
}

我们测试一下查询

    @Test
    public void testLogicDeleteQuery() {
        List<User> list = userService.list();
        System.out.println(list);
    }

结果:只会返回deleted=0的数据


我们再测试一下删除

    @Test
    public void testLogincDeleteDelete() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", "张三");
        boolean remove = userService.remove(queryWrapper);
        System.out.println(remove);
    }

结果:所有name="张三"的deleted字段都置为了1

注意:那么该如何插入的时候需要插入deleted字段么?需要,只不过角度不同,这里官方提供了三种方式:

  1. 字段在数据库定义默认值(推荐)
  2. insert 前自己 set 值
  3. 使用自动填充功能(这里下面会讲解到)

自动填充

在以前日常开发中,经常会出现一张表中有多个共用的字段,比如创建时间,创建人,最后更新时间,最后更新人,以及上面提到的逻辑删除标志位等等。这些共用的字段都可以通过MP自动填充来帮助我们填充,我们只需要关注那些特有字段即可,下面是具体代码

@Data
public abstract class BaseModel {

    @TableField(value = "create_by", fill = FieldFill.INSERT)
    private String createBy;

    @TableField(value = "update_by", fill = FieldFill.UPDATE)
    private String updateBy;

    @TableField(value = "deleted", fill = FieldFill.INSERT)
    private String deleted;

    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(value = "update_time", fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;
}

因为多个实体类有着共同的字段,所以我们抽取一个基类,里面全是共用的字段。其中有些字段是我们在插入的时候就需要设置,比如:创建人,创建时间,逻辑删除标志位,还有一些是更新时需要更新,比如:更新时间,更新人。这里用FieldFill来标识他们

@Data
@TableName(value = "company")
public class Company extends BaseModel {

    @TableId(type = IdType.AUTO)
    private Integer id;
    @TableField(value = "row_guid")
    private String rowGuid;
    @TableField(value = "company_name")
    private String companyName;
}

Company类中包含特有字段

public enum FieldFill {
    /**
     * 默认不处理
     */
    DEFAULT,
    /**
     * 插入填充字段
     */
    INSERT,
    /**
     * 更新填充字段
     */
    UPDATE,
    /**
     * 插入和更新填充字段
     */
    INSERT_UPDATE
}
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * 插入时填充
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        System.out.println("start insert fill....");
        this.strictInsertFill(metaObject, "createBy", String.class, "张三");
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "deleted", String.class, "0");
    }

    /**
     * 更新时填充
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        System.out.println("start update fill....");
        this.strictUpdateFill(metaObject, "updateBy", String.class, "王五");
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

要想实现自动填充的功能就需要我们自定义一个类去实现 MetaObjectHandler 覆盖其中的 insertFillupdateFill 方法,需要在其中给 strictInsertFillstrictUpdateFill传入4个参数,第一个是源对象(方法中传入的),第二个是数据库字段对应的成员属性名,第三个是成员属性的class对象,第四个是需要插入/更新的值


测试插入

    @Test
    public void testFill() {
        Company company = new Company();
        company.setRowGuid("123");
        company.setCompanyName("华为");
        int insert = companyMapper.insert(company);
        System.out.println(insert);
    }

结果:创建人,创建时间,逻辑删除字段都自动插入了


测试更新

    @Test
    public void testUpdateFill() {
        Company company = new Company();
        company.setRowGuid("1234");
        company.setCompanyName("华为1");
        QueryWrapper<Company> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("company_name", "华为");
        int update = companyMapper.update(company, queryWrapper);
        System.out.println(update);
    }

结果:更新人,更新时间都自动更新了


注意事项:

  • 填充原理是直接给entity的属性设置值!!!
  • 注解则是指定该属性在对应情况下必有值,如果无值则入库会是null
  • MetaObjectHandler提供的默认方法的策略均为:如果属性有值则不覆盖,如果填充值为null则不填充
  • 字段必须声明TableField注解,属性fill选择对应策略,该声明告知Mybatis-Plus需要预留注入SQL字段
  • 填充处理器MyMetaObjectHandler在 Spring Boot 中需要声明@Component或@Bean注入
  • 要想根据注解FieldFill.xxx和字段名以及字段类型来区分必须使用父类的strictInsertFill或者strictUpdateFill方法
  • 不需要根据任何来区分可以使用父类的fillStrategy方法
  • update(T t,Wrapper updateWrapper)时t不能为空,否则自动填充失效

执行SQL分析打印

平时开发我都是用日志来查看SQL的,如下:

logging:
	level:
		mybatisplusdemo: debug

接下来我们看看MP的SQL语句分析打印吧

首先要引入依赖

<dependency>
  <groupId>p6spy</groupId>
  <artifactId>p6spy</artifactId>
  <version>最新版本</version>
</dependency>

修改配置文件

server:
	port: 8181
# DataSource Config
spring:
	datasource:
		driver-class-name: com.p6spy.engine.spy.P6SpyDriver
			url: jdbc:p6spy:mysql://127.0.0.1:3306/mybatis_plus?useSSL=false&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF8&autoReconnect=true&failOverReadOnly=false
			username: root
			password: 123456

编写p6spy配置文件spy.properties

modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 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
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2

测试

@Test
    public void testLogicDeleteQuery() {
    List<User> list = userService.list();
    System.out.println(list);
}

控制台输出

 Consume Time2 ms 2022-10-05 14:43:53
 Execute SQLSELECT id,name,age,email,create_time,deleted FROM user WHERE deleted=0

如果是用Logback打印的话则是这样

2022-10-05 14:47:08.257 DEBUG 30880 --- [           main] mybatisplusdemo.UserMapper.selectList    : ==>  Preparing: SELECT id,name,age,email,create_time,deleted FROM user WHERE deleted=0
2022-10-05 14:47:08.286 DEBUG 30880 --- [           main] mybatisplusdemo.UserMapper.selectList    : ==> Parameters: 
2022-10-05 14:47:08.298 DEBUG 30880 --- [           main] mybatisplusdemo.UserMapper.selectList    : <==      Total: 0

感觉两者差不多,如果使用p6spy打印语句还是引入额外的依赖,所以使用SpringBoot集成的日志框架会比较便捷一点

由于作者能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!