【MP】常用注解知多少,结合使用场景版

前言

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。MyBatis Plus 是目前很多公司为了简化开发、提升开发效率持久层框架神器,作为 CRUD 程序员,熟练使用它非常重要!那就从 MP 常用注解开始吧! (p≧w≦q) 从使用场景出发,亲自实践,看完你一定收获满满。

常用注解以及使用场景

常用于类上

@TableName 表名注解

  • 当实体类名和数据库表名不一致时,使用 value 来指定实体类和数据库表之间的映射关系,

代码举例:

@TableName("数据库表名")
public class xxx {
    ...
}
复制代码

@TableName(value="数据库表名")
public class xxx {
    ...
}
复制代码

常用于属性上

@TableId 表主键注解

当表中主键和实体类中主键字段名一致时,可忽略该注释,MP 会自动识别,不用去特地显式指定。反之则使用 value 属性指定数据库表映射到实体类上哪一个是主键字段。

除了指定主键字段以外,还能使用 type 指定主键生成策略,默认策略 IdType.NONEinsert 前自行 set 主键值,如果没有给主键赋值,并且未设置主键自增,就看是否有默认值,没有就会报错。

常用主键生成策略:

  • IdType.AUTO 主键自增,需要和数据库表设置一致,否则 Field 'xxx' doesn't have a default value 安排!

  • IdType.ASSIGN_ID 主键分配 ID,主键 Java 类型要是 Number(Long和Integer)String,通过雪花算法得到 19 全局唯一 ID

  • IdType.ASSIGN_UUID 主键分配 UUID,主键 java 类型要求为 String

使用场景举例:

数据库中 tb_user 表:

CREATE TABLE `tb_user` (
  `id` bigint(64) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `user_uuid` varchar(32) DEFAULT NULL COMMENT '用户uuid',
  `user_name` varchar(20) DEFAULT NULL COMMENT '用户名',
  `password` varchar(20) DEFAULT NULL COMMENT '用户密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
复制代码

表映射到实体类上:

注意:@TableId 注解可以标注在并不是主键的属性字段上,但是在一个类中不能同时出现两个以上

@TableName(value = "tb_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;
    /**
    * 主键id
    */
    // @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
    * 用户uuid
    */
    @TableId(value = "user_uuid", type = IdType.ASSIGN_UUID)
    private String userUuid;

    /**
    * 用户名
    */
    private String userName;

    /**
    * 用户密码
    */
    private String password;
}
复制代码

新增记录(用户名和用户密码)后,表数据:

id user_uuid user_name password
1 cf204ada021e11f6887c865c96967733 悠悠球 323uj

插入时 iduser_uuid 均为被赋值,但插入记录后,id 跟随表主键自增设置,从 1 开始,而 user_uuid 则是走 MP 的主键策略,会自动生成一个 UUID

那能不能通过雪花算法生成一个主键 id 值,并用这个值作为后续插入行记录的主键起始值呢?

答案当然是肯定的,可以通过 MP 中实现类为 DefaultIdentifierGenerator 中的 nextId() 方法可以得到一个由雪花算法生成的长整型数值。

User user = new User();
user.setUserName("HUALEI");
user.setPassword("123456");
DefaultIdentifierGenerator generator = new DefaultIdentifierGenerator();
Long id = generator.nextId(user);
user.setId(id);
userMapper.insert(user);
复制代码

通过控制台可以很轻易地看出,生成的主键 id 并不是一个短整型,而是一个足足有 19 位的长整型数。

==>  Preparing: INSERT INTO tb_user ( user_uuid, id, user_name, password ) VALUES ( ?, ?, ?, ? ) 
==> Parameters: fdf8c23862755f91a1fbc8385b449c94(String), 1453644498497929218(Long), HUALEI(String), 123456(String)
<==    Updates: 1
复制代码

之后,只要不覆盖主键 id ,之后新增记录的主键 id 等于上一条记录的主键值加 1

@TableField 非主键字段注解

value
  • 注解在某一字段上,使用 value 指定实体类中的字段和数据库表的列的映射关系,下划线转驼峰命名时可以不用写,会自动形成映射。

使用场景举例:

数据库表的列名为 password ,但和实体类中的字段名并不一致:

/**
* 用户密码
*/
@TableField(value = "password")
private String userPassword;
复制代码

运行查询语句后,可以在控制台看见 :

SELECT user_uuid,id,user_name,password AS userPassword FROM tb_user WHERE user_uuid=?
复制代码

使用 @TableField 注解作用就是将表列名通过 as 取别名的方法,使结果集中字段映射到实体类的属性上。

exist
  • 使用 exist 指定实体类中的字段是否为数据库表字段,被标记的字段插入记录时会被忽略。

使用场景举例:

想使用该字段存储一些相关的额外信息,但是又不想在数据库表中新增列。

/**
 * 用户反馈(表中无对应列)
 */
@TableField(exist = false)
private String userFeedback;
复制代码

插入一条记录:

User user = new User();
user.setUserName("刚子");
user.setPassword("987654321");
user.setUserFeedback("滴滴滴");
userMapper.insert(user);
复制代码

即使,在 user 中给 userFeedback 设置了值,MP 还是会将其忽略不作为插入值。

condition
  • 通过 condition 属性预处理 WHERE 实体条件自定义规则,通过 SqlCondition

选择实现自定义条件。

使用场景举例:

仅适用注解完成对指定字段的模糊查询,不使用 Wrapper

/**
* 用户名
*/
@TableField(condition = SqlCondition.LIKE)
private String userName;
复制代码

用实体条件进行查询测试:

User user = new User();
user.setUserName("麻");
List<User> users = userMapper.selectList(new QueryWrapper<>(user));
System.out.println(users);
复制代码

image.png

没毛病,查询时用 %s 占位进行模糊查询:

==>  Preparing: SELECT user_uuid,id,user_name,password AS userPassword FROM tb_user WHERE user_name LIKE CONCAT('%',?,'%') 
==> Parameters: 麻(String)
<==    Columns: user_uuid, id, user_name, userPassword
<==        Row: a56ca26ec06932cd2b64dc06305c9909, 1453644498497929219, 王麻子, sdfd323
<==      Total: 1
复制代码
fill
  • 通过 fill 指定,字段为空时会通过 FieldFill 选择填充策略进行自动填充。
public enum FieldFill {
    /**
     * 默认不处理
     */
    DEFAULT,
    /**
     * 插入填充字段
     */
    INSERT,
    /**
     * 更新填充字段
     */
    UPDATE,
    /**
     * 插入和更新填充字段
     */
    INSERT_UPDATE
}
复制代码

使用场景举例:

更新一条记录时,自动填充更新时间,不用手动赋值。

@TableField(value = "update_time", fill = FieldFill.UPDATE)
private Date updateTime;
复制代码

执行更新操作后,自动填充更新时间,前提是更新时间为空:

==>  Preparing: UPDATE tb_user SET user_name=?, password=?, update_time=? WHERE user_uuid=? 
==> Parameters: 皮皮虾(String), password(String), 2021-10-29 11:27:07.752(Timestamp), 3fcf2ead00347248b85821baf5a23704(String)
<==    Updates: 1
复制代码

注意:也可以自行配置生成器策略部分

  1. 首先,实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler ,注意使用 @Component@Bean 注解注入到 Spring 容器中。
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    .....
}
复制代码
  1. 自定义实现类 MyMetaObjectHandler ,完成插入 / 更新时填充字段逻辑。

使用场景举例:

当我更新或者插入一条新数据时,数据库表中的 update_time 字段自动填充为当前时间戳。

实体类中对应的属性:

@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
复制代码

MyMetaObjectHandler 实现类中的逻辑:

@Override
public void insertFill(MetaObject metaObject) {
    log.info("start insert fill ....");
    this.setFieldValByName("updateTime", new Date(), metaObject);
}

@Override
public void updateFill(MetaObject metaObject) {
    log.info("start update fill ....");
    insertAndUpdate(metaObject, "updateTime");
}

private boolean ifAutoFill(MetaObject metaObject ,String fieldName) {
    Object fieldValue = metaObject.getValue(fieldName);
    return fieldValue == null;
}

private void insertAndUpdate(MetaObject metaObject, String fieldName) {
    // 如果属性未被赋值,则自动填充为当前时间
    if (ifAutoFill(metaObject, fieldName)) {
        this.setFieldValByName(fieldName, new Date(), metaObject);
    }
}
复制代码
select
  • 通过 select 指定该字段是否作为查询字段。

使用场景举例:

忽略查询字段 insert_time

@TableField(select = false)
private Date insertTime;
复制代码

查询 SQL 及结果:

==>  Preparing: SELECT user_uuid,id,user_name,password AS userPassword,update_time FROM tb_user WHERE user_name LIKE CONCAT('%',?,'%') 
==> Parameters: 麻(String)
<==    Columns: user_uuid, id, user_name, userPassword, update_time
<==        Row: a56ca26ec06932cd2b64dc06305c9909, 1453644498497929219, 王麻子, sdfd323, 2021-10-20 05:38:04
<==      Total: 1
复制代码

@Version 乐观锁注解

  • 使用 @Verison 注解标记在字段上,该字段用于控制唯一的修改操作,避免修改数据冲突。

  • 支持的数据类型只有:int、Integer、long、Long、Date、Timestamp、LocalDateTime

  • 整数类型下 newVersion = oldVersion + 1

  • newVersion 会回写到 entity 中

  • 仅支持 updateById(id) 与 update(entity, wrapper) 方法

使用场景举例:

当有人在修改同一条记录时,只会有一个人修改成功,只有当修改成功后,后面修改操作才会生效。

数据库表:

CREATE TABLE `tb_user` (
  `id` bigint(64) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `user_uuid` varchar(32) DEFAULT NULL COMMENT '用户uuid',
  `user_name` varchar(20) DEFAULT NULL COMMENT '用户名',
  `password` varchar(20) DEFAULT NULL COMMENT '用户密码',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `insert_time` datetime DEFAULT NULL COMMENT '新增时间',
  `version` int(11) DEFAULT '1' COMMENT '乐观锁标识',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1453644498497929222 DEFAULT CHARSET=utf8;
复制代码

实体类标记指定字段:

/**
* 乐观锁标识
*/
 @Version
 private Integer version;
复制代码

配置乐观锁拦截器:

@Configuration
public class MybatisPlusConfig {

    /**
     * 配置乐观锁拦截器
     * @return OptimisticLockerInterceptor 乐观锁拦截器对象
     */
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
        return new OptimisticLockerInterceptor();
    }
}
复制代码

测试用例:

User user1 = userMapper.selectById("fdf8c23862755f91a1fbc8385b449c94");
user1.setPassword("password1");

User user2 = userMapper.selectById("fdf8c23862755f91a1fbc8385b449c94");
user2.setPassword("password2");

userMapper.updateById(user1);
userMapper.updateById(user2);
复制代码

当修改数据时,会以 version 作为条件,当条件成立的时候才会修改成功,否则不允许修改。

==>  Preparing: UPDATE tb_user SET id=?, user_name=?, password=?, update_time=?, version=? WHERE user_uuid=? AND version=? 
==> Parameters: 1453644498497929218(Long), HUALEI(String), password1(String), 2021-10-29 14:58:19.0(Timestamp), 2(Integer), fdf8c23862755f91a1fbc8385b449c94(String), 1(Integer)
<==    Updates: 1
复制代码

修改同一条记录,userMapper.updateById(user1)userMapper.updateById(user2) 前,所以先判断 version = 1 条件成立修改密码成功,后面再次判断同样的条件,这时 version = 2 ,条件不成立故更新操作失败。

==>  Preparing: UPDATE tb_user SET id=?, user_name=?, password=?, update_time=?, version=? WHERE user_uuid=? AND version=? 
==> Parameters: 1453644498497929218(Long), HUALEI(String), password2(String), 2021-10-29 14:58:19.0(Timestamp), 2(Integer), fdf8c23862755f91a1fbc8385b449c94(String), 1(Integer)
<==    Updates: 0
复制代码

@EnumValue 枚举字段注解

  • 通常使用 @EnumValue 注解标记在枚举字段上,将数据库中的字段映射为实体类的枚举类型成员变量。

使用场景举例:

数据库表中通过 status 记录用户状态(0.离线 1.在线),字段值希望映射成枚举类型,通过 code 值关联状态。

通过 @EnumValue 指定数据库中的字段映射到目标属性,通过值关联得到 status 字符串。

@Getter
public enum UserStatusEnum {

    OFFLINE(0, "离线"),
    ONLINE(1, "上线");

    @EnumValue
    private Integer code;

    private String status;
}
复制代码

修改 resources/application.yml ,配置枚举包扫描:

type-enums-package: com.xxx.xxx.enums
复制代码

测试用例:

User user = userMapper.selectById("fdf8c23862755f91a1fbc8385b449c94");
System.out.println(user.getStatus().getStatus()); // 离线

user.setStatus(UserStatusEnum.ONLINE);
userMapper.updateById(user);
System.out.println(user.getStatus().getStatus()); // 上线
复制代码

查询记录:

==>  Preparing: SELECT user_uuid,id,user_name,password AS userPassword,status,update_time,version FROM tb_user WHERE user_uuid=? 
==> Parameters: fdf8c23862755f91a1fbc8385b449c94(String)
<==    Columns: user_uuid, id, user_name, userPassword, status, update_time, version
<==        Row: fdf8c23862755f91a1fbc8385b449c94, 1453644498497929218, HUALEI, password1, 0, 2021-10-29 14:58:19, 2
<==      Total: 1
复制代码

更新状态:

==>  Preparing: UPDATE tb_user SET id=?, user_name=?, password=?, status=?, update_time=?, version=? WHERE user_uuid=? AND version=? 
==> Parameters: 1453644498497929218(Long), HUALEI(String), password1(String), 1(Integer), 2021-10-29 14:58:19.0(Timestamp), 3(Integer), fdf8c23862755f91a1fbc8385b449c94(String), 2(Integer)
<==    Updates: 1
复制代码

上面这种方式是通过 @EnumValue 注解枚举属性 方式实现的,也可以不用注解通过实现 IEnum 接口的方式实现,泛型和数据库字段类型保持一致

实现接口后,重写 getValue() 方法,

public enum AgeEnum implements IEnum<Integer> {
    ONE(1, "一岁"),
    TWO(2, "二岁"),
    THREE(3, "三岁");
    
    private int value;
    private String desc;
    
    AgeEnum(Integer value, String desc) {
        this.value = value;
        this.desc = desc;
    }
    
    @Override
    public Integer getValue() {
        return this.value;
    }
}
复制代码

查询返回的实体类对象:

User [Hash = 1000592566, id=1453644498497929218, userUuid=fdf8c23862755f91a1fbc8385b449c94, userName=HUALEI, 
age=ONE, password=password1, status=ONLINE, feedback=null, updateTime=Fri Oct 29 14:58:19 CST 2021, insertTime=null, version=3]
复制代码

@TableLogic 映射逻辑删除注解

  • 使用 @TableLogic 注解标记逻辑删除字段,删除记录时(实则是做的事更新操作)只是将数据库该字段置为另一个值表示已被删除,该条记录实则还存在数据库表中。逻辑删除是为了方便数据恢复和保护数据本身价值,如果需要频繁查看就不应使用逻辑删除,而是用一个状态去表示。

使用场景举例:

数据库表的逻辑删除字段:

`is_del` tinyint(4) DEFAULT '0' COMMENT '逻辑删除 0. 未删除 1. 已删除'
复制代码

对应实体类中的字段,支持 Integer Boolean LocalDateTime 数据类型,这里使用布尔值并加上逻辑删除注解:

/**
 * 逻辑删除字段,0.未删除 1.已删除
 */
@TableLogic
private Boolean isDel;
复制代码

添加配置,如果配置了 logic-delete-field: isDel 表示指定全局逻辑删除的实体字段名为 isDelsince 3.3.0),配置了全局,可以不用再每个有该字段的实体类中添加注解和写成员变量。

mybatis-plus:
  global-config:
    db-config:
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
复制代码

执行删除操作后再次查询删除前的记录,查询的条件中会判断 is_del=0 ,即查询的每条记录都是逻辑上存在未被删除的。

==>  Preparing: UPDATE tb_user SET is_del=1 WHERE user_uuid=? AND is_del=0 
==> Parameters: fdf8c23862755f91a1fbc8385b449c94(String)
<==    Updates: 1

==>  Preparing: SELECT user_uuid,id,user_name,age,password AS userPassword,status,update_time,version,is_del FROM tb_user WHERE user_uuid=? AND is_del=0 
==> Parameters: fdf8c23862755f91a1fbc8385b449c94(String)
<==      Total: 0
复制代码

结尾

撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。

参考

注解 | MyBatis-Plus (baomidou.com)