首页 > 技术文章 > mybatis-plus进阶版

likeguang 2021-12-18 23:01 原文

1、实体类注解

介绍一下mybatis-plus中提供的注解

mp一共提供了8个注解,这些注解是用在Java的实体类上面的。

  • @TableName

    注解在类上,指定类和数据库表的映射关系。实体类的类名(转成小写后)和数据库表名相同时 ,可以不指定该注解。

  • @TableId

    注解在实体类的某一字段上,表示这个字段对应数据库表的主键 。当主键名为id时(表中列名为id,实体类中字段名为id),无需使用该注解显式指定主键,mp会自动关联。若类的字段名和表的列名不一致,可用value属性指定表的列名。另,这个注解有个重要的属性type,用于指定主键策略,参见主键策略小节

  • @TableField

    注解在某一字段上,指定Java实体类的字段和数据库表的列的映射关系。这个注解有如下几个应用场景。

    • 排除非表字段

      若Java实体类中某个字段,不对应表中的任何列,它只是用于保存一些额外的,或组装后的数据,则可以设置exist属性为false,这样在对实体对象进行插入时,会忽略这个字段。排除非表字段也可以通过其他方式完成,如使用statictransient关键字,但个人觉得不是很合理,不做赘述

    • 字段验证策略

      通过insertStrategyupdateStrategywhereStrategy属性进行配置,可以控制在实体对象进行插入,更新,或作为WHERE条件时,对象中的字段要如何组装到SQL语句中。参见配置小节

    • 字段填充策略

      通过fill属性指定,字段为空时会进行自动填充。参见配置小节

  • @Version

    乐观锁注解,参见乐观锁小节

  • @EnumValue

    注解在枚举字段上

  • @TableLogic

    逻辑删除,参见逻辑删除小节

  • KeySequence

    序列主键策略(oracle

  • InterceptorIgnore

    插件过滤规则

2、主键策略

在定义实体类时,用@TableId指定主键,而其type属性,可以指定主键策略。

mp支持多种主键策略,默认的策略是基于雪花算法的自增id。全部主键策略定义在了枚举类IdType中,IdType有如下的取值

  • AUTO

    数据库ID自增,依赖于数据库 。在插入操作生成SQL语句时,不会插入主键这一列

  • NONE

    未设置主键类型。若在代码中没有手动设置主键,则会根据主键的全局策略 自动生成(默认的主键全局策略是基于雪花算法的自增ID)(在初级篇中使用的就是雪花算法来生成对应的ID的)

  • INPUT

    需要手动设置主键,若不设置。插入操作生成SQL语句时,主键这一列的值会是null。oracle的序列主键需要使用这种方式

  • ASSIGN_ID

    当没有手动设置主键,即实体类中的主键属性为空时,才会自动填充,使用雪花算法

  • ASSIGN_UUID

    当实体类的主键属性为空时,才会自动填充,使用UUID

  • ....(还有几种是已过时的,就不再列举)

可以针对每个实体类,使用@TableId注解指定该实体类的主键策略,这可以理解为局部策略 。若希望对所有的实体类,都采用同一种主键策略,挨个在每个实体类上进行配置,则太麻烦了,此时可以用主键的全局策略 。只需要在application.yml进行配置即可。比如,配置了全局采用自增主键策略。

# application.yml
mybatis-plus:
  global-config:
    db-config:
      id-type: auto

下面对不同主键策略的行为进行演示:

  • AUTO

User上对id属性加上注解,然后将MYSQL的user表修改其主键为自增。如果不设置成自增,那么会报错。

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class User extends Model<User> {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

测试

    @Test
    public void testIdAuto(){
        User user = new User();
        user.setName("我是青蛙呱呱");
        user.setAge(99);
        user.setEmail("frog@baomidou.com");
        user.insert();
        System.out.println(user);  // User(id=7, name=我是青蛙呱呱, age=99, email=frog@baomidou.com)
    }

如果插入成功,这里还会将数据进行回显。

  • NONE

    在MYSQL的user表中,去掉主键自增。然后修改User类(若不配置@TableId注解,默认主键策略也是NONE

    @TableId(type = IdType.NONE)
    private Long id;
    

    插入时,若实体类的主键ID有值,则使用之;若主键ID为空,则使用主键全局策略,来生成一个ID。

    这种是在入门版中看到的模式。

  • 其余的策略类似,不赘述

小结:

AUTO依赖于数据库的自增主键,插入时,实体对象无需设置主键,插入成功后,主键会被写回实体对象。

INPUT完全依赖于用户输入。实体对象中主键ID是什么,插入到数据库时就设置什么。若有值便设置值,若为null则设置null

其余的几个策略,都是在实体对象中主键ID为空时,才会自动生成。

NONE会跟随全局策略,ASSIGN_ID采用雪花算法,ASSIGN_UUID采用UUID

全局配置,在application.yml中进行即可;针对单个实体类的局部配置,使用@TableId即可。对于某个实体类,若它有局部主键策略,则采用之,否则,跟随全局策略。

3、配置

mybatis plus有许多可配置项,可在application.yml中进行配置,如上面的全局主键策略。下面列举部分配置项

基本配置

  • configLocation:若有单独的mybatis配置,用这个注解指定mybatis的配置文件(mybatis的全局配置文件)
  • mapperLocations:mybatis mapper所对应的xml文件的位置
  • typeAliasesPackage:mybatis的别名包扫描路径
  • .....

进阶配置

  • mapUnderscoreToCamelCase:是否开启自动驼峰命名规则映射。(默认开启)

  • dbTpe:数据库类型。一般不用配,会根据数据库连接url自动识别

  • fieldStrategy:(已过时)字段验证策略。该配置项在最新版的mp文档中已经找不到了 ,被细分成了insertStrategyupdateStrategyselectStrategy。默认值是NOT_NULL,即对于实体对象中非空的字段,才会组装到最终的SQL语句中。

    有如下几种可选配置

    这个配置项,可在application.yml中进行全局配置 ,也可以在某一实体类中,对某一字段用@TableField注解进行局部配置

    这个字段验证策略有什么用呢?在UPDATE操作中能够体现出来,若用一个User对象执行UPDATE操作,我们希望只对User对象中非空的属性,更新到数据库中,其他属性不做更新,则NOT_NULL可以满足需求。而若updateStrategy配置为IGNORED,则不会进行非空判断,会将实体对象中的全部属性如实组装到SQL中,这样,执行UPDATE时,可能就将一些不想更新的字段,设置为了NULL

    • IGNORED:忽略校验。即,不做校验。实体对象中的全部字段,无论值是什么,都如实地被组装到SQL语句中(为NULL的字段在SQL语句中就组装为NULL)。
    • NOT_NULL:非NULL校验。只会将非NULL的字段组装到SQL语句中
    • NOT_EMPTY:非空校验。当有字段是字符串类型时,只组装非空字符串;对其他类型的字段,等同于NOT_NULL
    • NEVER:不加入SQL。所有字段不加入到SQL语句
  • tablePrefix:添加表名前缀

比如:

mybatis-plus:
  global-config:
    db-config:
      table-prefix: xx_

然后将MYSQL中的表做一下修改。但Java实体类保持不变(仍然为User)。

那么来进行查询一下:

    @Test
    public void testQuery(){
        User user  = new User();
        List<User> userList = user.selectAll();
        userList.forEach(System.out::println);
    }

查看对应的SQL语句:

SELECT id,name,age,email FROM xx_user

发现这里会帮助我们自动来进行拼接。这里这里的设计主要是考虑到了早期的设计当中,都会来加入对应的tb_标志来设计表。

4、逻辑删除

参考官网说明:

说明:

只对自动注入的 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()
附录:

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

使用方法

步骤 1: 配置com.baomidou.mybatisplus.core.config.GlobalConfig$DbConfig

例: application.yml

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

步骤 2: 实体类字段上加上@TableLogic注解

@TableLogicprivate Integer deleted;

参考官方文档说明,如果在application.yaml中配置了logic-delete-field的字段,那么这里可以忽略,当然看一下版本。

再看一下官方提供的提供的一些问题:

常见问题

1、如何 insert ?

这里提供了三种使用方式:

(1)字段在数据库定义默认值(推荐)

(2)insert 前自己 set 值

(3)使用自动填充功能

那么看一下官方提供的使用自动填充功能API:https://baomidou.com/pages/4c6bcf/

1、实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler2、注解填充字段 @TableField(.. fill = FieldFill.INSERT) 生成器策略部分也可以配置!    
public class User {    // 注意!这里需要标记为填充字段    @TableField(.. fill = FieldFill.INSERT)    private String fillField;    ....}
  • 自定义实现类 MyMetaObjectHandler
@Slf4j@Componentpublic class MyMetaObjectHandler implements MetaObjectHandler {    @Override    public void insertFill(MetaObject metaObject) {        log.info("start insert fill ....");        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());         // 起始版本 3.3.0(推荐使用)        // 或者        this.strictInsertFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class);         // 起始版本 3.3.3(推荐)        // 或者        this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)    }    @Override    public void updateFill(MetaObject metaObject) {        log.info("start update fill ....");        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());         // 起始版本 3.3.0(推荐)        // 或者        this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class);         // 起始版本 3.3.3(推荐)        // 或者        this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)    }}
注意事项:填充原理是直接给entity的属性设置值!!!      // 在执行到配置类中的时候会去对其做一个设置值;注解则是指定该属性在对应情况下必有值,如果无值则入库会是null   // 不设置就为Null,设置了就是我们设置的值;MetaObjectHandler提供的默认方法的策略均为:如果属性有值则不覆盖,如果填充值为null则不填充字段必须声明TableField注解,属性fill选择对应策略,该声明告知Mybatis-Plus需要预留注入SQL字段填充处理器MyMetaObjectHandler在 Spring Boot 中需要声明@Component或@Bean注入要想根据注解FieldFill.xxx和字段名以及字段类型来区分必须使用父类的strictInsertFill或者strictUpdateFill方法不需要根据任何来区分可以使用父类的fillStrategy方法
public enum FieldFill {    /**     * 默认不处理     */    DEFAULT,    /**     * 插入填充字段     */    INSERT,    /**     * 更新填充字段     */    UPDATE,    /**     * 插入和更新填充字段     */    INSERT_UPDATE}

但是感觉一个小小的逻辑删除不需要来设置的这么复杂。

下一个demo来测试一下

自动填充配置类:

@Slf4j@Componentpublic class MyMetaObjectHandler implements MetaObjectHandler {    /**     * 严格填充,只针对非主键的字段,只有该表注解了fill 并且 字段名和字段属性 能匹配到才会进行填充(null 值不填充)     * @param metaObject     */    @Override    public void insertFill(MetaObject metaObject) {        // ==>  Preparing: INSERT INTO xx_user ( name, age, email, is_del, create_time, create_username ) VALUES ( ?, ?, ?, ?, ?, ? )        //==> Parameters: 我是青蛙呱呱6666(String), 998(Integer), frog@baomidou.com(String), 0(Integer), 2021-12-18T18:23:57.543(LocalDateTime), null        log.info("start insert fill ....");        // 设置插入的实践        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)        // 设置逻辑删除的值         this.strictInsertFill(metaObject, "isDel", Integer.class, 0); // 起始版本 3.3.0(推荐使用)        // 设置添加的用户名称         this.strictInsertFill(metaObject, "createUserName", String.class, "system"); // 起始版本 3.3.0(推荐使用)    }    /**     * 设置成逻辑删除     * 注意一下,这里的fieldName为pojo中的属性     * @param metaObject     */    @Override    public void updateFill(MetaObject metaObject) {        this.strictUpdateFill(metaObject, "updateUserName", LocalDateTime.class, LocalDateTime.now());        this.strictUpdateFill(metaObject, "isDel", Integer.class, 1);        this.strictUpdateFill(metaObject, "updateUserName", String.class, "guang");        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());    }}

对应的pojo:

@Data@NoArgsConstructor@AllArgsConstructor@Builder(toBuilder = true)@Accessors(chain = true)@EqualsAndHashCode(callSuper = false)public class User extends Model<User> {    @TableId(type = IdType.AUTO)    private Long id;    private String name;    private Integer age;    private String email;    /**     * 数据库中五大必备字段     */    @TableField(value = "is_del",fill = FieldFill.INSERT_UPDATE)    private Integer isDel;    @TableField(value = "create_time",fill = FieldFill.INSERT)    private LocalDateTime createTime;    @TableField(value = "create_username",fill = FieldFill.INSERT)    private String createUserName;    @TableField(value = "update_username",fill = FieldFill.UPDATE)    private String updateUserName;    @TableField(value = "update_time",fill = FieldFill.UPDATE)    private LocalDateTime updateTime;}

这里还需要将yaml中的来进行设置一下:

mybatis-plus.global-config.db-config.tablePrefix=xx_mybatis-plus.global-config.db-config.logic-delete-field=isDelmybatis-plus.global-config.db-config.logic-delete-value=1mybatis-plus.global-config.db-config.logic-not-delete-value=0
    @Test    public void testIdAuto() {        User user = new User();        user.setName("我是青蛙呱呱6666");        user.setAge(998);        // 这里不能够手动来进行设置。如果这里手动设置了,那么逻辑删除的插入将不会执行        // user.setIsDel(999);        user.setEmail("frog@baomidou.com");        user.insert();        System.out.println(user);    }    @Test    public void testUpdateAuto() {        User user = new User();        user.setId(19L);        user.setName("我是青蛙呱呱333");        user.setAge(996);        user.setEmail("frog@baomidou.com");        user.updateById();    }    @Test    public void testDeleteAuto() {        User user = new User();        user.setId(15L);        user.deleteById();    }

看一下控制台显示testDeleteAuto:

JDBC Connection [HikariProxyConnection@1657512321 wrapping com.mysql.cj.jdbc.ConnectionImpl@1b482cbf] will not be managed by Spring==>  Preparing: UPDATE xx_user SET is_del=1 WHERE id=? AND is_del=0==> Parameters: 15(Long)<==    Updates: 0

从这里可以看到在进行删除的也是可以的

同时在mybatis-plus针对删除接口失效问题来提供了对应的解决方法那么参考着下面的案例:

2. 删除接口自动填充功能失效

(1)使用 update 方法并: UpdateWrapper.set(column, value)(推荐)
(2)使用 update 方法并: UpdateWrapper.setSql("column=value")
(3)使用 Sql 注入器 注入 com.baomidou.mybatisplus.extension.injector.methods.LogicDeleteByIdWithFill 并使用(推荐)

看一下第三种方式,这种方式是比较采用来进行使用的。

官方让我们参考DefaultSqlInjector 这个类来进行操作,那么就看一下对应的实现方式:

public class LogicDeleteHandler extends AbstractSqlInjector {    @Override    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {        return Stream.<AbstractMethod>builder().add(new LogicDeleteByIdWithFill()).build().collect(toList());    }}

那么将这些记录保存下来,方便以后来进行使用。

逻辑删除配置、新增人名称、新增时间、修改人名称、修改时间

    /**     * 数据库中五大必备字段     */    @TableField(value = "is_del",fill = FieldFill.INSERT_UPDATE)    private Integer isDel;    @TableField(value = "create_time",fill = FieldFill.INSERT)    private LocalDateTime createTime;    @TableField(value = "create_username",fill = FieldFill.INSERT)    private String createUserName;    @TableField(value = "update_username",fill = FieldFill.UPDATE)    private String updateUserName;    @TableField(value = "update_time",fill = FieldFill.UPDATE)    private LocalDateTime updateTime;

针对于逻辑删除的yaml配置:

mybatis-plus.global-config.db-config.logic-delete-field=isDelmybatis-plus.global-config.db-config.logic-delete-value=1mybatis-plus.global-config.db-config.logic-not-delete-value=0

对应的元数据配置类:

@Slf4j@Componentpublic class MyMetaObjectHandler implements MetaObjectHandler {    /**     * 严格填充,只针对非主键的字段,只有该表注解了fill 并且 字段名和字段属性 能匹配到才会进行填充(null 值不填充)     * @param metaObject     */    @Override    public void insertFill(MetaObject metaObject) {        // ==>  Preparing: INSERT INTO xx_user ( name, age, email, is_del, create_time, create_username ) VALUES ( ?, ?, ?, ?, ?, ? )        //==> Parameters: 我是青蛙呱呱6666(String), 998(Integer), frog@baomidou.com(String), 0(Integer), 2021-12-18T18:23:57.543(LocalDateTime), null        log.info("start insert fill ....");        // 设置插入的实践        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)        // 设置逻辑删除的值         this.strictInsertFill(metaObject, "isDel", Integer.class, 0); // 起始版本 3.3.0(推荐使用)        // 设置添加的用户名称         this.strictInsertFill(metaObject, "createUserName", String.class, "system"); // 起始版本 3.3.0(推荐使用)    }    /**     * 设置成逻辑删除     * 注意一下,这里的fieldName为pojo中的属性     * @param metaObject     */    @Override    public void updateFill(MetaObject metaObject) {        this.strictUpdateFill(metaObject, "updateUserName", LocalDateTime.class, LocalDateTime.now());         this.strictUpdateFill(metaObject, "isDel", Integer.class, 1);        this.strictUpdateFill(metaObject, "updateUserName", String.class, "guang");        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());    }}

5、乐观锁

参考官方文档:https://baomidou.com/pages/0d93c0/

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

取出记录时,获取当前 version
更新时,带上这个 version
执行更新时, set version = newVersion where version = oldVersion
如果 version 不对,就更新失败
乐观锁配置需要两步

1.配置插件

@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());    return interceptor;}

在实体类的字段上加上@Version注解

@Versionprivate Integer version;
说明:支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime整数类型下 newVersion = oldVersion + 1newVersion 会回写到 entity 中仅支持 updateById(id) 与 update(entity, wrapper) 方法在 update(entity, wrapper) 方法下, wrapper 不能复用!!!

官网有一个例子:

// Spring Boot 方式@Configuration@MapperScan("按需修改")public class MybatisPlusConfig {    /**     * 新版     */    @Bean    public MybatisPlusInterceptor mybatisPlusInterceptor() {        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());        return mybatisPlusInterceptor;    }      }

6、分页插件

网上连个demo都没有给,但是给了一个自定义的 mapper#method 使用分页。

这里先来记录一下:

https://baomidou.com/pages/97710a/#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%9A%84-mapper-method-%E4%BD%BF%E7%94%A8%E5%88%86%E9%A1%B5

那么将二者来进行配置一下:

// Spring Boot 方式@Configuration@MapperScan("按需修改")public class MybatisPlusConfig {    /**     * 新版     */    @Bean    public MybatisPlusInterceptor mybatisPlusInterceptor() {        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());        return mybatisPlusInterceptor;    }        @Bean    public OptimisticLockerInterceptor optimisticLockerInterceptor() {        return new OptimisticLockerInterceptor();    }    }

乐观锁的使用,这个需要结合着事务来进行描述了。

https://zhuanlan.zhihu.com/p/31537871

jwt:https://mp.weixin.qq.com/s/yuGkpO32QUbwhzgB55VUng

参考博文:https://mp.weixin.qq.com/s/SBkYZrBbGEgBe09erNr7tg、https://baomidou.com/pages/24112f/

推荐阅读