Skip to content

枚举字段与 ValueEnum

宠物有三种状态:在售、已售、下架。这类「值域固定、需要落库、前端还要展示中文名」的字段,在 forge 里统一用枚举建模——枚举实现 ValueEnum<T> 接口,字段上标 @Field.Enum。入库存的是枚举给定的标量值(PetStatus 存 1/2/3),不是枚举名,更不是 ordinal() 序号。

本篇沿用同一套宠物商店实体,聚焦枚举字段从声明到落库的完整闭环,照抄即可跑通。@Field 系列其余字段类型见 模型注解 与本章 总览

第一步:枚举实现 ValueEnum

落库枚举必须实现 cn.cvking.forge.metadata.annotation.ValueEnum<T>。普通 Java 枚举不允许落库,会在启动期校验失败。接口约定三个方法:getValue() 返回入库标量值,getDisplayName() 返回前端展示名(必须提供),getHelper() 返回帮助说明(可选,缺省空串)。

java
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.INTValueEnum<String> 推断为 STRINGValueEnum<Long> 推断为 LONG。你不需要在注解里显式声明 storeType,改泛型即改存储类型。推断逻辑见 FieldTypeHelper.resolveEnumStoreType()

第二步:字段上标 @Field.Enum

Pet 里把 status 字段声明为枚举字段。@Field 给业务元信息(显示名、是否非空),@Field.Enum 声明这是个枚举列。

java
@Field(displayName = "状态")
@Field.Enum
private PetStatus status;

@Field.Enum 还有两个可选参数,用于多选场景:

参数默认值含义
multiSerialize()COMMA多选时的序列化方式:COMMA 逗号分隔、JSON JSON 数组、BITMASK 位掩码
length()50枚举列长度

Pet.status 是单选,用默认值即可,无需关心 multiSerialize

第三步:写入并验证库里存的是 1/2/3

写一只在售的宠物,看看库里实际存了什么。

java
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 日志:

yaml
# 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

text
==>  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 列存的是标量值:

sql
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

下一步

  • 其余字段类型(@Field.String / @Field.BigDecimal / @Field.Date 等)→ 模型注解
  • 把枚举字段写进库、再查出来 → CRUD 操作
  • 想看「为什么枚举不让用 ordinal、为什么 storeType 走泛型推断」→ 设计文档