搜索 K
Appearance
Appearance
宠物有三种状态:在售、已售、下架。这类「值域固定、需要落库、前端还要展示中文名」的字段,在 forge 里统一用枚举建模——枚举实现 ValueEnum<T> 接口,字段上标 @Field.Enum。入库存的是枚举给定的标量值(PetStatus 存 1/2/3),不是枚举名,更不是 ordinal() 序号。
本篇沿用同一套宠物商店实体,聚焦枚举字段从声明到落库的完整闭环,照抄即可跑通。@Field 系列其余字段类型见 模型注解 与本章 总览。
落库枚举必须实现 cn.cvking.forge.metadata.annotation.ValueEnum<T>。普通 Java 枚举不允许落库,会在启动期校验失败。接口约定三个方法:getValue() 返回入库标量值,getDisplayName() 返回前端展示名(必须提供),getHelper() 返回帮助说明(可选,缺省空串)。
package com.demo.petshop.model;
import cn.cvking.forge.metadata.annotation.ValueEnum;
public enum PetStatus implements ValueEnum<Integer> {
ON_SALE(1, "在售"),
SOLD(2, "已售"),
OFF_SHELF(3, "下架");
private final Integer value;
private final String displayName;
PetStatus(Integer value, String displayName) {
this.value = value;
this.displayName = displayName;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String getDisplayName() {
return displayName;
}
@Override
public String getHelper() {
return "";
}
}storeType 由泛型推断,无需手写
注意 PetStatus implements ValueEnum<Integer>。框架从这个泛型实参推断标量存储类型——ValueEnum<Integer> 推断为 EnumStoreType.INT,ValueEnum<String> 推断为 STRING,ValueEnum<Long> 推断为 LONG。你不需要在注解里显式声明 storeType,改泛型即改存储类型。推断逻辑见 FieldTypeHelper.resolveEnumStoreType()。
在 Pet 里把 status 字段声明为枚举字段。@Field 给业务元信息(显示名、是否非空),@Field.Enum 声明这是个枚举列。
@Field(displayName = "状态")
@Field.Enum
private PetStatus status;@Field.Enum 还有两个可选参数,用于多选场景:
| 参数 | 默认值 | 含义 |
|---|---|---|
multiSerialize() | COMMA | 多选时的序列化方式:COMMA 逗号分隔、JSON JSON 数组、BITMASK 位掩码 |
length() | 50 | 枚举列长度 |
Pet.status 是单选,用默认值即可,无需关心 multiSerialize。
写一只在售的宠物,看看库里实际存了什么。
Pet pet = new Pet();
pet.setName("旺财");
pet.setStatus(PetStatus.ON_SALE); // 期望入库存 1
pet.setPrice(new BigDecimal("199.00"));
Models.of(pet).save();要在控制台看到生成的 SQL,先在 application.yml 打开 MyBatis-Plus 的 SQL 日志:
# application.yml
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
com.demo.petshop: debug
cn.cvking.forge: debug启动并执行上面的写入后,控制台会打印形如下面的 INSERT,status 列绑定的参数是整数 1,而不是字符串 ON_SALE、也不是序号 0:
==> Preparing: INSERT INTO pet ( name, status, price, create_time, update_time ) VALUES ( ?, ?, ?, ?, ? )
==> Parameters: 旺财(String), 1(Integer), 199.00(BigDecimal), 2026-06-04 10:00:00.0(LocalDateTime), 2026-06-04 10:00:00.0(LocalDateTime)直接查库进一步确认 status 列存的是标量值:
SELECT id, name, status, price FROM pet WHERE id = 1;
-- status 列值为 1(ON_SALE),SOLD 为 2,OFF_SHELF 为 3回读时框架按 getValue() 反查枚举常量,pet.getStatus() 还原为 PetStatus.ON_SALE,业务层始终拿到的是枚举对象,看不到 1/2/3 这层细节。
枚举字段解析后落入 FieldDefinition,并随启动期元数据写入系统表 sys_field。其中与枚举相关的列:
| 字段 | 含义 | PetStatus 取值 |
|---|---|---|
fieldType | 字段业务类型(FieldType 枚举) | 枚举类型 |
enumClass | 枚举类全限定名 | com.demo.petshop.model.PetStatus |
enumStoreType | 标量存储类型(由泛型推断) | INT |
columnType | 数据库列类型(由方言映射) | 整型列 |
想理解推断与落库的内部链路
本篇只讲怎么用。EnumStoreType 如何从泛型实参反射推断、sys_field 在启动期如何落库,属于实现细节,见 设计文档;其中涉及的反射读取泛型,可参考 前置知识 · Java 反射。
反例 1:枚举没实现 ValueEnum
现象:启动期 fail-fast 抛异常,应用起不来。
原因:Pet.status 用了一个普通 Java 枚举(没有 implements ValueEnum<Integer>)。框架不允许普通枚举落库——没有 getValue() 就无法确定入库标量值,框架拒绝猜测。
修复:让枚举实现 ValueEnum<T>,提供 getValue() / getDisplayName() / getHelper(),如本篇第一步所示。
反例 2:storeType 与字段实际值不符
现象:库里存了值,但回读时报数值越界、类型转换失败,或唯一值莫名截断。
原因:泛型实参与实际 value 不匹配。比如声明成 ValueEnum<Integer>(推断为 INT 列),却在构造里塞了字符串编码或超出整型范围的值。storeType 完全由泛型实参推断,列类型据此生成,与你 getValue() 真正返回的东西脱节就会出问题。
修复:让泛型实参与 getValue() 返回类型严格一致。需要字符串编码就改成 implements ValueEnum<String> 并让 getValue() 返回 String,列类型会随之变为字符串列。
反例 3:用 ordinal 当入库值
现象:调整枚举常量顺序、或在中间插入了一个新常量后,老数据语义全部错位——原本「在售」的记录读出来变成了别的状态。
原因:误把 getValue() 实现成返回 ordinal()(声明顺序的下标)。ordinal() 是 Java 枚举的声明位置,与业务语义无关,一旦增删或调序就整体平移。
修复:为每个常量显式指定稳定的业务值(如本篇 ON_SALE(1, ...)),getValue() 返回这个显式值,永远不要返回 ordinal()。显式值一经上线就不要再改。
反例 4:getDisplayName 返回 null
现象:枚举能落库,但管理端展示该字段时报空指针或显示空白。
原因:getDisplayName() 返回了 null。该方法是前端展示用的必填项,不能为空。
修复:每个常量都提供非空的中文展示名(如「在售」「已售」「下架」);确实没有帮助文案时,让 getHelper() 返回空串而非 null。