搜索 K
Appearance
Appearance
在 forge 中,一张表、一个字段的全部定义都写在 Java 类上:@Model 描述模型,@Field 描述字段。框架在启动期扫描这些注解,生成 ModelDefinition 与 FieldDefinition,再据此建表、落库元数据。本篇以宠物商店的 Pet、Category 为例,把这套注解从头到尾讲清楚,给出可直接复制的代码与启动后的真实日志。
关联阅读
注解如何被反射扫描、何时触发建表,属于内部实现,本篇不展开,可跳到 模型解析与注册(设计篇) 与 启动钩子前置知识。
先看分类 Category,它只有三个标量字段,足以演示 @Model 与基础 @Field。
package com.demo.petshop.model;
import cn.cvking.forge.metadata.annotation.Field;
import cn.cvking.forge.metadata.annotation.Model;
import cn.cvking.forge.data.model.IdModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Model(name = "pet.Category", displayName = "分类")
public class Category extends IdModel<Category> {
@Field(displayName = "名称")
@Field.String(length = 64)
private String name;
@Field(displayName = "编码")
@Field.String(length = 32)
private String code;
}两点约定:
IdModel<T>,自动获得 id 主键以及 createTime、updateTime 审计字段,无需自己声明。@Field(给 displayName 等)和一个类型子注解(如 @Field.String)。两者缺一不可——只写 @Field 而漏掉类型子注解,启动期无法推断列类型,会报错。@Model 标在类上,声明这是一个 forge 模型。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name | String | "" | 模型名,为空时取类简名,最终与模块编码组合成全局唯一标识 |
displayName | String | "" | 显示名称,前端与文档展示用 |
summary | String | "" | 描述摘要 |
命名约定
name 推荐写成 模块前缀.类名 的形式,如宠物商店统一用 pet.Pet、pet.Category,便于在系统元数据里按模块归类。
需要指定表名、逻辑删除、索引时,再加一个 @Model.Advanced。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
type | ModelTypeEnum | STORE | 模型类型:STORE(落库)/ TRANSIENT(瞬态)/ ABSTRACT(抽象基类)/ PROXY(代理) |
table | String | "" | 显式表名,为空时由模型名驼峰转下划线推导 |
logicDelete | LogicDelete | DEFAULT | 逻辑删除开关:DEFAULT(跟随全局)/ ENABLED / DISABLED |
logicDeleteColumn | String | "" | 逻辑删除标记列名,为空时取全局配置(默认 deleted) |
index | String[] | {} | 普通索引,逗号分隔多字段,如 {"ownerId", "ownerId,status"} |
unique | String[] | {} | 唯一索引,写法同上,如 {"code"} |
宠物 Pet 给状态加一个普通索引、给分类编码加唯一约束的写法:
@Model(name = "pet.Pet", displayName = "宠物")
@Model.Advanced(
index = {"status", "ownerId"},
unique = {"name"}
)
public class Pet extends IdModel<Pet> {
// 字段略
}唯一索引与逻辑删除
当模型启用逻辑删除时,唯一索引会在末尾自动追加逻辑删除列,使其语义变为「未删数据内唯一」,软删后可复用同一个唯一值。无需手工处理,框架在 IndexDefinition 层面完成。详见 逻辑删除与删表删列(设计篇)。
@Field 标在字段上,承载字段的通用语义。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
displayName | String | "" | 字段显示名 |
summary | String | "" | 描述摘要 |
notNull | boolean | false | 是否非空约束,对应建表 NOT NULL |
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
store | boolean | true | 是否持久化到数据库,置为 false 则不生成数据库列(纯应用层字段、关系字段用) |
column | String | "" | 显式列名,为空时由字段名驼峰转下划线推导 |
name 字段非空、并显式指定列名的写法:
@Field(displayName = "名称", notNull = true)
@Field.Advanced(column = "pet_name")
@Field.String(length = 64)
private String name;每个落库标量字段必须挂一个类型子注解,框架据此推断数据库列类型。
| 子注解 | 适用 Java 类型 | 关键参数(默认值) | 说明 |
|---|---|---|---|
@Field.String | String | length (255) | 变长字符串 |
@Field.Text | String | 无 | 长文本(TEXT) |
@Field.LongText | String | 无 | 超长文本(LONGTEXT) |
@Field.Integer | Integer | 无 | 整型 |
@Field.Long | Long | 无 | 长整型 |
@Field.Boolean | Boolean | 无 | 布尔 |
@Field.BigDecimal | BigDecimal | precision (19)、scale (4) | 精确小数,precision 为总位数含小数 |
@Field.Date | 日期时间类型 | type (DATETIME) | DATE / DATETIME / TIMESTAMP / TIME |
@Field.Enum | ValueEnum 子类 | multiSerialize (COMMA)、length (50) | 枚举,详见枚举一节 |
宠物售价用 @Field.BigDecimal,总位数 10、小数位 2:
@Field(displayName = "售价")
@Field.BigDecimal(precision = 10, scale = 2)
private BigDecimal price;@Field(displayName = "上架时间")
@Field.Date(type = Field.DateType.DATETIME)
private LocalDateTime onSaleTime;审计字段自动填充
IdModel 已用 @Field.Date 声明了 createTime、updateTime,并配合 MyBatis-Plus 的填充策略在插入/更新时自动写入,业务字段无需重复声明。
forge 不允许普通 Java 枚举直接落库——所有持久化枚举字段的枚举类型必须实现 ValueEnum<T> 接口,否则启动期校验失败。宠物状态枚举 PetStatus:
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 "";
}
}字段侧只需挂 @Field.Enum,无需声明存储类型:
@Field(displayName = "状态")
@Field.Enum
private PetStatus status;存储标量类型由 ValueEnum<T> 的泛型实参自动推断——ValueEnum<Integer> 入 INT 列、ValueEnum<String> 入字符串列、ValueEnum<Long> 入长整型列。多选枚举的序列化方式由 multiSerialize 控制,可选 COMMA(逗号分隔,默认)、JSON、BITMASK。
ValueEnum<T> 三个方法的职责:
| 方法 | 职责 |
|---|---|
getValue() | 入库标量值 |
getDisplayName() | 显示名称,前端展示用,必须提供 |
getHelper() | 帮助说明,可选,缺省返回空串 |
宠物与分类、主人、档案、标签的关系,用 @Field.M2O / @Field.O2O / @Field.O2M / @Field.M2M 配合 @Field.Relation 声明,关系字段是虚拟字段、不落库(store=false),且双向关系字段需加 @EqualsAndHashCode.Exclude、@ToString.Exclude 防止 Lombok 生成的方法循环递归导致 StackOverflowError。
// 宠物所属分类:M2O,外键 categoryId 落在 Pet 侧
@Field.M2O
@Field.Relation(relationFields = "categoryId")
@EqualsAndHashCode.Exclude
@ToString.Exclude
private Category category;关系字段的完整参数表、读填充与写维护用法,见 关系字段(使用篇)。本篇聚焦标量字段。
在 application.yml 打开框架日志,观察启动期模型扫描与元数据落库:
logging:
level:
cn.cvking.forge: debug启动后控制台会打印生命周期与元数据落库的统计行(数字随实际模型数变化):
[lifecycle] Module topology order: [pet]
Lifecycle=DDL, 模块 1 个, 扫描模型 7 个, 耗时 312ms
[sys_meta] 元数据落库完成,耗时 48 ms(模块 1 个、模型 7 个)落库后,可在系统元数据表里看到对应记录:sys_model 一行对应 Pet(含 tableName、primaryKey、logicDelete、uniqueKeys 等列),sys_field 多行对应每个字段(含 columnName、fieldType、columnType、enumClass 等)。关系字段也会作为虚拟字段写入 sys_field,但其 columnType 为空。
反例 1:只写 @Field 漏掉类型子注解
现象:启动期报错,字段类型无法确定。 原因:@Field 只承载 displayName、notNull 等通用语义,不携带列类型信息;列类型靠 @Field.String 等子注解推断。 修复:为每个落库标量字段补上对应的类型子注解。
反例 2:用普通枚举落库
现象:枚举字段在启动期校验失败。 原因:forge 强制持久化枚举实现 ValueEnum<T>,框架要从泛型实参推断存储类型、从 getValue() 取入库值;普通 Java 枚举不满足此契约。 修复:让枚举 implements ValueEnum<Integer>(或 <String> / <Long>),实现 getValue、getDisplayName、getHelper。
反例 3:字段名撞数据库保留字
现象:建表或查询时数据库报语法错误。 原因:字段名驼峰转下划线后的列名恰好是保留字(如 order、desc)。 修复:用 @Field.Advanced(column = "...") 显式指定一个安全的列名,避开保留字。
反例 4:关系字段漏掉 store=false 与 Exclude
现象:保存时多出无意义的列,或 equals/toString 触发 StackOverflowError。 原因:关系字段是虚拟字段不应落库;双向关系互相引用,Lombok 生成的 equals/hashCode/toString 会无限递归。 修复:关系字段用 @Field.Relation(隐含不落库),并加 @EqualsAndHashCode.Exclude、@ToString.Exclude。详见 关系字段(使用篇)。