Skip to content

快速开始

本篇用三步带你跑通 forge 的数据建模:引依赖、确认启动类扫描包、写出第一个 @Model,再用 Models 门面存一条、查一条。全程以宠物商店为例,先从最简单的分类实体 Category 起步。

你将得到什么

读完本篇,你会有一张由 Category 自动建出的表、一条写入的数据,以及启动控制台里「扫描模型 N 个」的验证日志。关系字段、枚举落库等进阶能力另见后续篇章。

第一步:引入依赖

数据建模只需两个核心模块:forge-metadata 提供注解与元数据模型,forge-data 提供 Models 门面与关系读写能力。

xml
<dependency>
    <groupId>cn.cvking.forge</groupId>
    <artifactId>forge-metadata</artifactId>
</dependency>
<dependency>
    <groupId>cn.cvking.forge</groupId>
    <artifactId>forge-data</artifactId>
</dependency>

版本与构建

版本号通常由父 POM 统一管理,此处不写死。模块的安装顺序与 JDK 要求见 构建与环境

第二步:确认启动类扫描包

forge 在启动期通过包扫描发现所有标注了 @Model 的类。务必让你的实体所在包处于扫描范围内,否则模型不会被注册,也就不会建表。

java
package com.demo.petshop;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.demo.petshop")
public class PetShopApplication {
    public static void main(String[] args) {
        SpringApplication.run(PetShopApplication.class, args);
    }
}

扫描包是第一道坎

实体类 com.demo.petshop.model.Category 必须落在 scanBasePackages 声明的包(或其子包)之下。这是后面「常见反例」里第一个会踩的坑。包扫描的触发时机与启动钩子原理见 启动与扫描机制

第三步:写第一个 @Model

下面是宠物商店的分类实体 Category,包含 namecode 两个业务字段。它继承 IdModel<T>,自动获得 id 主键和 createTimeupdateTime 审计字段。

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 = 64)
    private String code;
}

要点说明:

  • 类上的 @Model 标记它是一个模型,name 为模型唯一标识、displayName 为中文显示名。name 缺省时取类简名,这里显式写成 pet.Category 与业务模块对齐。
  • 每个要落库的字段都需要 @Field 描述显示名,并叠加一个类型子注解(如 @Field.String)声明数据库类型。@Field.Stringlength 默认 255,这里收紧到 64。
  • 不必手写 id 与表名。id 来自 IdModel<T> 基类,表名缺省由类名驼峰转下划线推导(即 category)。

用 Models 门面存一条、查一条

Models 是统一的 CRUD 静态门面。写入用 Models.of(entity).save(),查询用 Models.origin(Class) 链式构造条件。

java
import cn.cvking.forge.data.Models;
import com.demo.petshop.model.Category;

// 存一条
Category dog = new Category();
dog.setName("狗狗");
dog.setCode("DOG");
Models.of(dog).save();   // 主键为 null 走 insert,落库后回填 id

// 按 id 查一条
Category found = Models.origin(Category.class).queryById(dog.getId());

// 按条件查一条
Category byCode = Models.origin(Category.class)
        .where(w -> w.eq(Category::getCode, "DOG"))
        .queryOne();

save 的语义

Models.of(entity).save() 会按主键是否存在自动分流:主键为 null 执行 insert,否则执行 updateById,并按唯一键兜底。需要批量写入时用 Models.ofBatch(collection).saveBatch()

验证结果

application.yml 打开 forge 的 debug 日志,便于观察启动期的模型扫描与建表链路:

yaml
logging:
  level:
    cn.cvking.forge: debug

启动应用,控制台会打印模块拓扑与扫描到的模型数量。典型日志形态如下(数量随你的实体数变化):

text
[lifecycle] Module topology order: [pet]
Lifecycle=DDL, 模块 1 个, 扫描模型 1 个, 耗时 128ms
[sys_meta] 元数据落库完成,耗时 36 ms(模块 1 个、模型 1 个)

看到「扫描模型 1 个」且数字与你的实体数一致,说明 Category 已被正确发现并注册。随后执行 save() 时,控制台会打印类似下面的写入 SQL:

sql
INSERT INTO category (name, code, create_time, update_time) VALUES ('狗狗', 'DOG', ?, ?);

查询 queryById 则打印(forge 默认对启用逻辑删除的表追加删除列过滤,未启用时无此条件):

sql
SELECT * FROM category WHERE id = ?;
看不到 SQL?补上 MyBatis-Plus 的 SQL 日志

forge 底层基于 MyBatis-Plus。若想看到完整 SQL,可把对应 mapper 包的日志级别也调到 debug,或在 yml 中配置 MyBatis-Plus 的标准 SQL 输出。模型扫描日志只依赖 cn.cvking.forge 这一项。

常见反例与排查

反例一:实体不在扫描包内

现象:启动日志里「扫描模型 N 个」的数字比预期少,对应的表也没建出来,调用 Models.origin(Category.class) 时按未注册模型报错。

原因:Category 所在包不在启动类 scanBasePackages 声明的范围内,包扫描没找到它。

修复:把实体移入扫描包,或扩大 scanBasePackages。例如启动类声明 com.demo.petshop,实体放在 com.demo.petshop.model 即可被覆盖。

反例二:类上忘了写 @Model

现象:类已经在扫描包里,但「扫描模型 N 个」仍不计入它,建表与 CRUD 都不生效。

原因:包扫描只挑选标注了 @Model 的类,没有该注解的普通 POJO 会被直接跳过。

修复:在类上补 @Model(name = "pet.Category", displayName = "分类")。建议 name 显式写成「模块前缀.类名」便于在系统元数据中辨识。

反例三:字段忘了写 @Field

现象:模型注册成功、表建出来了,但某个字段没有对应的数据库列,存进去后再查为 null

原因:只有标注了 @Field 且带类型子注解(如 @Field.String)的字段才会被纳入元数据并生成列。漏标的字段不参与持久化。

修复:给该字段补上 @Field 与类型子注解。若是有意不落库的纯应用层字段,则用 @Field.Advanced(store = false) 显式声明,避免被误判为遗漏。

下一步

  • 想了解关系字段(CategoryOwnerTag 等如何挂到 Pet 上)的写法,继续看关系建模相关指南。
  • 想理解模型扫描、元数据落库与 DDL 链路「为什么这么设计」,见数据建模的设计文档。