第 55 章:自动填充与枚举
学习目标
- 实现创建/更新时间与操作人自动填充
- 掌握枚举与数据库值的映射
- 学会 JSON 字段与自定义类型处理器
一、自动填充:告别重复 setter
// ❌ 每次插入都手动设置四个字段
user.setCreateTime(LocalDateTime.now());
user.setCreateBy(currentUserId);
user.setUpdateTime(LocalDateTime.now());
user.setUpdateBy(currentUserId);
userMapper.insert(user);第 1 步:实体标注
@Data
public class BaseEntity {
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT)
private Long createBy;
@TableField(fill = FieldFill.INSERT_UPDATE) // ① 插入和更新都填
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
@TableLogic
@TableField(select = false) // ② 查询时不返回这个字段
private Integer deleted;
}
@Data
@EqualsAndHashCode(callSuper = true) // ③ Lombok:继承时必须加
@TableName("sys_user")
public class User extends BaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String username;
}第 2 步:实现填充处理器
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
LocalDateTime now = LocalDateTime.now();
// ① strictInsertFill:字段有值时不覆盖(推荐,允许手动指定)
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, now);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, now);
Long userId = getCurrentUserId();
if (userId != null) {
this.strictInsertFill(metaObject, "createBy", Long.class, userId);
this.strictInsertFill(metaObject, "updateBy", Long.class, userId);
}
}
@Override
public void updateFill(MetaObject metaObject) {
// ② 更新时间强制覆盖,不允许手动指定
this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
Long userId = getCurrentUserId();
if (userId != null) {
this.setFieldValByName("updateBy", userId, metaObject);
}
}
/** ③ 从安全上下文取当前用户,取不到就返回 null(定时任务、系统初始化场景) */
private Long getCurrentUserId() {
try {
return SecurityUtils.getCurrentUserId();
} catch (Exception e) {
return null;
}
}
}| 方法 | 行为 |
|---|---|
strictInsertFill | 字段为 null 时才填充 |
setFieldValByName | 强制覆盖 |
③ 为什么要 try-catch? 定时任务、消息消费、系统启动时的数据初始化都没有登录用户。不处理会抛异常导致整个插入失败。
⚠️ 自动填充不生效的场景
// ① UpdateWrapper 不带实体时,updateFill 不触发
userService.lambdaUpdate()
.eq(User::getId, 1L)
.set(User::getStatus, 0)
.update(); // ❌ updateTime 不会自动更新
// ✅ 手动加上
userService.lambdaUpdate()
.eq(User::getId, 1L)
.set(User::getStatus, 0)
.set(User::getUpdateTime, LocalDateTime.now())
.update();
// ② 自定义 XML 的 SQL 完全不走填充器
// ③ 数据库层面的默认值(DEFAULT CURRENT_TIMESTAMP)和这个是两套机制兜底方案:数据库字段也设默认值,双保险。
sqlcreate_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
二、枚举映射
问题:数据库存数字,代码里想用枚举
// ❌ 到处是魔法值
if (user.getStatus() == 1) { }
user.setStatus(0);方案 1:@EnumValue(推荐)
@Getter
public enum UserStatus {
DISABLED(0, "禁用"),
NORMAL(1, "正常"),
LOCKED(2, "锁定");
@EnumValue // ① 标记:这个字段的值存入数据库
private final int code;
@JsonValue // ② 标记:序列化给前端时用这个字段
private final String desc;
UserStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
/** ③ 反序列化:前端传值时如何转成枚举 */
@JsonCreator
public static UserStatus of(int code) {
return Arrays.stream(values())
.filter(e -> e.code == code)
.findFirst()
.orElseThrow(() -> new BusinessException(
ErrorCode.PARAM_INVALID, "非法的状态值: " + code));
}
}mybatis-plus:
type-enums-package: com.taskflow.**.enums # ① 扫描枚举包@Data
public class User {
private UserStatus status; // ② 直接用枚举类型
}
// 使用
user.setStatus(UserStatus.NORMAL); // 存入数据库的是 1
if (user.getStatus() == UserStatus.NORMAL) { } // 类型安全,IDE 有提示JSON 返回效果:
{ "status": "正常" } // 因为 @JsonValue 标在 desc 上设计权衡:返回
"正常"前端展示方便,但前端做逻辑判断时用中文字符串很脆弱(改文案就崩)。 更好的做法:返回对象{"status": 1, "statusName": "正常"}——code 给逻辑用,name 给展示用。
// 实现方式:VO 里放两个字段
@Data
public class UserVO {
private Integer status;
private String statusName;
}方案 2:实现 IEnum 接口
public enum UserStatus implements IEnum<Integer> {
DISABLED(0, "禁用"),
NORMAL(1, "正常");
private final int code;
private final String desc;
@Override
public Integer getValue() { // 实现这个方法即可
return code;
}
}枚举校验注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValueValidator.class)
public @interface EnumValue {
Class<? extends Enum<?>> enumClass();
String method() default "getCode";
String message() default "参数值不在允许范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
private Class<? extends Enum<?>> enumClass;
private String method;
@Override
public void initialize(EnumValue anno) {
this.enumClass = anno.enumClass();
this.method = anno.method();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext ctx) {
if (value == null) return true;
Object[] constants = enumClass.getEnumConstants();
if (constants == null) return false;
try {
Method m = enumClass.getMethod(method);
for (Object constant : constants) {
if (Objects.equals(m.invoke(constant), value)) return true;
}
} catch (Exception e) {
return false;
}
return false;
}
}@EnumValue(enumClass = UserStatus.class, message = "状态值非法")
private Integer status;三、JSON 字段映射
场景:一个字段存 JSON(如用户扩展属性、商品规格)。
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY,
ext_info JSON, -- MySQL 5.7+ 支持 JSON 类型
tags VARCHAR(500) -- 或者用字符串存
);@Data
@TableName(value = "sys_user", autoResultMap = true) // ① 必须开启 autoResultMap
public class User {
@TableField(typeHandler = JacksonTypeHandler.class) // ② 指定处理器
private UserExtInfo extInfo; // 对象自动转 JSON
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> tags; // 列表也可以
}
@Data
public class UserExtInfo {
private String wechat;
private String github;
private Map<String, Object> custom;
}①
autoResultMap = true不能忘。不加的话查询时 TypeHandler 不生效,只有插入生效——表现为「存进去了但查出来是 null」。
JSON 字段的取舍
| 优点 | 缺点 |
|---|---|
| 灵活,加字段不用改表结构 | 无法建索引(MySQL 8 可用虚拟列变通) |
| 减少表数量 | 查询条件无法用 JSON 内部字段 |
| 适合稀疏属性 | 数据一致性靠应用层保证 |
使用原则:只用于不需要查询和统计的辅助信息。核心业务字段必须建成独立列。
四、自定义 TypeHandler
场景:数据库存逗号分隔字符串 "1,2,3",代码里想用 List<Long>。
@MappedTypes(List.class)
public class LongListTypeHandler extends BaseTypeHandler<List<Long>> {
private static final String DELIMITER = ",";
/** ① Java → 数据库 */
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
List<Long> parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter.stream()
.map(String::valueOf)
.collect(Collectors.joining(DELIMITER)));
}
/** ② 数据库 → Java */
@Override
public List<Long> getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return parse(rs.getString(columnName));
}
@Override
public List<Long> getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
return parse(rs.getString(columnIndex));
}
@Override
public List<Long> getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
return parse(cs.getString(columnIndex));
}
private List<Long> parse(String value) {
if (!StringUtils.hasText(value)) return new ArrayList<>();
return Arrays.stream(value.split(DELIMITER))
.filter(StringUtils::hasText)
.map(Long::valueOf)
.toList();
}
}@TableName(value = "sys_role", autoResultMap = true)
public class Role {
@TableField(typeHandler = LongListTypeHandler.class)
private List<Long> menuIds;
}⚠️ 逗号分隔字段是反范式设计。它让「查询拥有某菜单的所有角色」变成
LIKE '%,5,%'的全表扫描,而且边界匹配容易出错(5会匹配到15)。多对多关系应该建中间表。这个 TypeHandler 主要用于对接遗留系统。
五、字段加密(TypeHandler 实战)
场景:手机号、身份证在数据库中加密存储。
public class EncryptTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
String parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, AesUtils.encrypt(parameter)); // ① 写入前加密
}
@Override
public String getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return AesUtils.decrypt(rs.getString(columnName)); // ② 读取后解密
}
// 其他重载方法同理
}@TableField(typeHandler = EncryptTypeHandler.class)
private String idCard;⚠️ 加密字段无法做精确查询和排序。如果需要按手机号查询,方案是:额外存一个
phone_hash字段(SHA256(phone + salt)),查询时比对 hash。
六、本章小结
| 要点 | 关键 |
|---|---|
| 自动填充 | @TableField(fill=...) + MetaObjectHandler |
| 填充失效 | UpdateWrapper、自定义 XML 不触发 |
| 兜底 | 数据库字段也设 DEFAULT |
| 枚举 | @EnumValue 标数据库值,@JsonValue 标前端值 |
| 枚举返回 | 推荐返回 code + name 两个字段 |
| JSON 字段 | 必须 autoResultMap = true |
| JSON 使用原则 | 只存不需要查询的辅助信息 |
| 逗号分隔 | 反范式,优先建中间表 |
| 字段加密 | 需查询时额外存 hash 列 |
动手练习
练习 1:基础题
实现 BaseEntity + MetaObjectHandler,让所有表的四个审计字段自动填充。验证 insert 和 update 都生效。
练习 2:进阶题
定义订单状态枚举(待支付/已支付/已发货/已完成/已取消),实现 @EnumValue 校验注解,并让接口返回 {"status":1,"statusName":"已支付"} 格式。
下一章:第 56 章:多租户与动态表名 →