Skip to content

模型与字段注解

在 forge 中,一张表、一个字段的全部定义都写在 Java 类上:@Model 描述模型,@Field 描述字段。框架在启动期扫描这些注解,生成 ModelDefinitionFieldDefinition,再据此建表、落库元数据。本篇以宠物商店的 PetCategory 为例,把这套注解从头到尾讲清楚,给出可直接复制的代码与启动后的真实日志。

关联阅读

注解如何被反射扫描、何时触发建表,属于内部实现,本篇不展开,可跳到 模型解析与注册(设计篇)启动钩子前置知识

一个最小可用的模型

先看分类 Category,它只有三个标量字段,足以演示 @Model 与基础 @Field

java
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 主键以及 createTimeupdateTime 审计字段,无需自己声明。
  • 每个要落库的字段,都要同时写一个标识字段语义的 @Field(给 displayName 等)和一个类型子注解(如 @Field.String)。两者缺一不可——只写 @Field 而漏掉类型子注解,启动期无法推断列类型,会报错。

@Model:描述模型本身

@Model 标在类上,声明这是一个 forge 模型。

参数类型默认值说明
nameString""模型名,为空时取类简名,最终与模块编码组合成全局唯一标识
displayNameString""显示名称,前端与文档展示用
summaryString""描述摘要

命名约定

name 推荐写成 模块前缀.类名 的形式,如宠物商店统一用 pet.Petpet.Category,便于在系统元数据里按模块归类。

@Model.Advanced:表级进阶配置

需要指定表名、逻辑删除、索引时,再加一个 @Model.Advanced

参数类型默认值说明
typeModelTypeEnumSTORE模型类型:STORE(落库)/ TRANSIENT(瞬态)/ ABSTRACT(抽象基类)/ PROXY(代理)
tableString""显式表名,为空时由模型名驼峰转下划线推导
logicDeleteLogicDeleteDEFAULT逻辑删除开关:DEFAULT(跟随全局)/ ENABLED / DISABLED
logicDeleteColumnString""逻辑删除标记列名,为空时取全局配置(默认 deleted
indexString[]{}普通索引,逗号分隔多字段,如 {"ownerId", "ownerId,status"}
uniqueString[]{}唯一索引,写法同上,如 {"code"}

宠物 Pet 给状态加一个普通索引、给分类编码加唯一约束的写法:

java
@Model(name = "pet.Pet", displayName = "宠物")
@Model.Advanced(
        index = {"status", "ownerId"},
        unique = {"name"}
)
public class Pet extends IdModel<Pet> {
    // 字段略
}

唯一索引与逻辑删除

当模型启用逻辑删除时,唯一索引会在末尾自动追加逻辑删除列,使其语义变为「未删数据内唯一」,软删后可复用同一个唯一值。无需手工处理,框架在 IndexDefinition 层面完成。详见 逻辑删除与删表删列(设计篇)

@Field:描述字段

@Field 标在字段上,承载字段的通用语义。

参数类型默认值说明
displayNameString""字段显示名
summaryString""描述摘要
notNullbooleanfalse是否非空约束,对应建表 NOT NULL

@Field.Advanced:列级进阶配置

参数类型默认值说明
storebooleantrue是否持久化到数据库,置为 false 则不生成数据库列(纯应用层字段、关系字段用)
columnString""显式列名,为空时由字段名驼峰转下划线推导

name 字段非空、并显式指定列名的写法:

java
@Field(displayName = "名称", notNull = true)
@Field.Advanced(column = "pet_name")
@Field.String(length = 64)
private String name;

类型子注解一览

每个落库标量字段必须挂一个类型子注解,框架据此推断数据库列类型。

子注解适用 Java 类型关键参数(默认值)说明
@Field.StringStringlength (255)变长字符串
@Field.TextString长文本(TEXT)
@Field.LongTextString超长文本(LONGTEXT)
@Field.IntegerInteger整型
@Field.LongLong长整型
@Field.BooleanBoolean布尔
@Field.BigDecimalBigDecimalprecision (19)、scale (4)精确小数,precision 为总位数含小数
@Field.Date日期时间类型type (DATETIME)DATE / DATETIME / TIMESTAMP / TIME
@Field.EnumValueEnum 子类multiSerialize (COMMA)、length (50)枚举,详见枚举一节

数值与金额:BigDecimal

宠物售价用 @Field.BigDecimal,总位数 10、小数位 2:

java
@Field(displayName = "售价")
@Field.BigDecimal(precision = 10, scale = 2)
private BigDecimal price;

日期时间:Date

java
@Field(displayName = "上架时间")
@Field.Date(type = Field.DateType.DATETIME)
private LocalDateTime onSaleTime;

审计字段自动填充

IdModel 已用 @Field.Date 声明了 createTimeupdateTime,并配合 MyBatis-Plus 的填充策略在插入/更新时自动写入,业务字段无需重复声明。

枚举:Enum 与 ValueEnum 契约

forge 不允许普通 Java 枚举直接落库——所有持久化枚举字段的枚举类型必须实现 ValueEnum<T> 接口,否则启动期校验失败。宠物状态枚举 PetStatus

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 "";
    }
}

字段侧只需挂 @Field.Enum,无需声明存储类型:

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

存储标量类型由 ValueEnum<T> 的泛型实参自动推断——ValueEnum<Integer>INT 列、ValueEnum<String> 入字符串列、ValueEnum<Long> 入长整型列。多选枚举的序列化方式由 multiSerialize 控制,可选 COMMA(逗号分隔,默认)、JSONBITMASK

ValueEnum<T> 三个方法的职责:

方法职责
getValue()入库标量值
getDisplayName()显示名称,前端展示用,必须提供
getHelper()帮助说明,可选,缺省返回空串

关系字段简述

宠物与分类、主人、档案、标签的关系,用 @Field.M2O / @Field.O2O / @Field.O2M / @Field.M2M 配合 @Field.Relation 声明,关系字段是虚拟字段、不落库(store=false),且双向关系字段需加 @EqualsAndHashCode.Exclude@ToString.Exclude 防止 Lombok 生成的方法循环递归导致 StackOverflowError

java
// 宠物所属分类:M2O,外键 categoryId 落在 Pet 侧
@Field.M2O
@Field.Relation(relationFields = "categoryId")
@EqualsAndHashCode.Exclude
@ToString.Exclude
private Category category;

关系字段的完整参数表、读填充与写维护用法,见 关系字段(使用篇)。本篇聚焦标量字段。

验证结果

application.yml 打开框架日志,观察启动期模型扫描与元数据落库:

yaml
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(含 tableNameprimaryKeylogicDeleteuniqueKeys 等列),sys_field 多行对应每个字段(含 columnNamefieldTypecolumnTypeenumClass 等)。关系字段也会作为虚拟字段写入 sys_field,但其 columnType 为空。

常见反例与排查

反例 1:只写 @Field 漏掉类型子注解

现象:启动期报错,字段类型无法确定。 原因:@Field 只承载 displayNamenotNull 等通用语义,不携带列类型信息;列类型靠 @Field.String 等子注解推断。 修复:为每个落库标量字段补上对应的类型子注解。

反例 2:用普通枚举落库

现象:枚举字段在启动期校验失败。 原因:forge 强制持久化枚举实现 ValueEnum<T>,框架要从泛型实参推断存储类型、从 getValue() 取入库值;普通 Java 枚举不满足此契约。 修复:让枚举 implements ValueEnum<Integer>(或 <String> / <Long>),实现 getValuegetDisplayNamegetHelper

反例 3:字段名撞数据库保留字

现象:建表或查询时数据库报语法错误。 原因:字段名驼峰转下划线后的列名恰好是保留字(如 orderdesc)。 修复:用 @Field.Advanced(column = "...") 显式指定一个安全的列名,避开保留字。

反例 4:关系字段漏掉 store=false 与 Exclude

现象:保存时多出无意义的列,或 equals/toString 触发 StackOverflowError。 原因:关系字段是虚拟字段不应落库;双向关系互相引用,Lombok 生成的 equals/hashCode/toString 会无限递归。 修复:关系字段用 @Field.Relation(隐含不落库),并加 @EqualsAndHashCode.Exclude@ToString.Exclude。详见 关系字段(使用篇)