Skip to content

API 速查

这是数据建模与关系字段相关 API 的速查表,用于忘记签名时快速回顾。所有示例复用宠物商店实体(PetCategoryOwnerPetProfileTagPetTag),包名 com.demo.petshop.model,框架注解来自 cn.cvking.forge.*

需要理解底层机制时,请跳转到对应设计篇:数据访问门面设计关系字段设计

TIP

所有写示例默认实体继承自 IdModel<T>(带 idcreateTimeupdateTime),编码模型继承自 CodeModel<T>(额外带唯一 code)。

Models 静态门面

Models 是所有 CRUD 与关系操作的统一入口(cn.cvking.forge.data.Models)。

方法签名用途
origin(Class<T>) 返回 OriginQuery<T>进入原生查询/删除/更新链
repository(Class<T>) 返回 Repository<T>取底层仓储句柄
of(T entity) 返回 EntityOps<T>单实体写操作入口
ofBatch(Collection<T>) 返回 BatchOps<T>批量写操作入口(集合与元素均非空、类型一致)
fieldQuery(T, String...) 返回 T就地填充单个实体的关系字段
listFieldQuery(List<T>, String...) 返回 List<T>就地填充列表中各元素的关系字段
saveWith(T, String...) 返回 T连同关系一并保存(回填外键、落库、同步 M2M)

最小示例:

java
// 查询
Pet pet = Models.origin(Pet.class).queryById(1L);

// 单实体保存(主键存在判库决定 insert/update)
Models.of(pet).save();

// 批量保存
Models.ofBatch(List.of(pet1, pet2)).saveBatch();

// 填充关系字段后读取(不传字段名则填全部关系字段)
Models.fieldQuery(pet, "category", "owner", "tags");

// 连同关系一并写入
Models.saveWith(pet, "category", "tags");

WARNING

ofBatch 会校验「集合非空、元素非空、元素类型一致」,混入异类元素会直接抛异常。

OriginQuery 查询与删改

Models.origin(Class<T>) 得到,支持链式条件 where(...)

方法签名用途最小示例
where(Consumer<LambdaQueryWrapper<T>>)追加链式条件.where(w -> w.eq(Pet::getStatus, ON_SALE))
wrapper() 返回 LambdaQueryWrapper<T>取底层包装器q.wrapper()
queryById(Serializable) 返回 T主键查单条q.queryById(1L)
queryOne() 返回 T条件查单条q.queryOne()
queryList() 返回 List<T>条件查列表q.queryList()
queryListByIds(Collection) 返回 List<T>主键集合查列表q.queryListByIds(ids)
queryPage(long current, long size) 返回 IPage<T>分页查询q.queryPage(1, 10)
count() 返回 long计数q.count()
exists() 返回 boolean是否存在q.exists()
deleteById(Serializable) 返回 int主键删除q.deleteById(1L)
deleteByIds(Collection) 返回 int主键集合删除q.deleteByIds(ids)
delete() 返回 int条件删除q.where(...).delete()
update(T entity) 返回 int条件更新q.where(...).update(pet)
queryIdMap() 返回 Map<Long, T>主键索引表q.queryIdMap()
queryByCode(String) 返回 T按编码查询(CodeModel 专用)q.queryByCode("CAT01")
queryCodeMap() 返回 Map<String, T>编码索引表(CodeModel 专用)q.queryCodeMap()
java
// 在售且价格低于 100 的宠物,按主人维度建索引
List<Pet> onSale = Models.origin(Pet.class)
        .where(w -> w.eq(Pet::getStatus, PetStatus.ON_SALE).lt(Pet::getPrice, 100))
        .queryList();

// CodeModel 子类按 code 查询(Category extends CodeModel)
Category dog = Models.origin(Category.class).queryByCode("DOG");

EntityOps 单实体写

Models.of(T) 得到。

方法签名用途
save() 返回 T主键存在判库决定 insert/update,并按唯一键兜底
insert() 返回 int强制插入
updateById() 返回 int按主键更新
java
Models.of(pet).save();      // 智能保存
Models.of(pet).insert();    // 强制插入

BatchOps 批量写

Models.ofBatch(Collection<T>) 得到。

方法签名用途
insertBatch() 返回 int批量插入
updateBatchById() 返回 int按主键批量更新
saveBatch() 返回 int按主键与唯一键分流 insert/update 批量执行
java
Models.ofBatch(pets).saveBatch();

RelationReadApi 关系读填充

读填充把外键还原为关系对象,对应实现 DefaultRelationReadApi。日常通过 Models.fieldQuery / Models.listFieldQuery 触发,下表给出四类关系的填充语义。

关系类型填充方法行为一句话生成 SQL 形态
M2O / O2OfillToOne收外键值,IN 查目标表,按被引用值建索引写回单个对象SELECT * FROM target_table WHERE ref_column IN (...)
O2MfillO2M收本模型被引用值,IN 查对端表,按对端外键分组写回集合SELECT * FROM target_table WHERE ref_column IN (...)
M2MfillM2M先 IN 查中间表得目标键,再 IN 查目标表,按映射写回集合中间表 + 目标表两次 IN 查询
java
// 单实体:填充宠物的分类、主人、档案、标签
Pet pet = Models.origin(Pet.class).queryById(1L);
Models.fieldQuery(pet, "category", "owner", "profile", "tags");

// 列表:一次性批量填充(内部按 IN 批量查,避免 N+1)
List<Pet> pets = Models.origin(Pet.class).queryList();
Models.listFieldQuery(pets, "owner", "tags");

// 主人的一对多反向填充
Owner owner = Models.origin(Owner.class).queryById(1L);
Models.fieldQuery(owner, "pets");
fillM2M 的两次 IN 查询(以宠物-标签为例)
sql
-- 1. 中间表:按宠物主键查关联行
SELECT * FROM pet_tag WHERE pet_id IN (1, 2, 3);
-- 2. 目标表:按收集到的标签键查标签
SELECT * FROM tag WHERE id IN (10, 11, 12);

RelationWriteApi 关系写维护

写维护对应实现 DefaultRelationWriteApi,由 Models.saveWith 编排,纯应用层完成,不依赖数据库外键约束。

方法关系类型时机行为一句话
applyToOneM2O / O2O本表落库前从关系对象读被引用值,回填本表外键列
syncM2MM2M本表落库后按关系集合与中间表现状做 diff,增删中间表行

saveWith 的内部顺序:先 applyToOne 回填外键,再 save() 落本表分配主键,最后 syncM2M 同步中间表。

java
Pet pet = new Pet();
pet.setName("旺财");
pet.setStatus(PetStatus.ON_SALE);
pet.setCategory(category);          // M2O:放对象,外键自动从 category.id 回填到 categoryId
pet.setTags(List.of(tag1, tag2));   // M2M:放对象集合,落库后同步 pet_tag

Models.saveWith(pet, "category", "tags");

两种保存形态与几条硬约束

  • M2O/O2O 支持「放对象」与「放 id」两种形态,二者并存且不一致时对象优先(对象被引用值覆盖已设外键)。
  • M2O/O2O 关系对象非空但其被关联字段为 null 时 fail-fast 抛异常。
  • M2M 元素必须是目标对象或仅含被引用字段/主键的「半对象」,不支持裸主键值集合(如 Set<Long>)。
  • M2M 同步要求本模型主键非空,否则抛「M2M 需主键非空」异常。
  • O2M 的存储维护落在对端模型的外键列上,不在本侧处理。
  • 当前仅支持单字段关联键,复合键会抛明确异常。

@Field.Relation 全参数

关系字段用 @Field.O2O / @Field.M2O / @Field.O2M / @Field.M2M 标注类型,用 @Field.Relation 配置映射关系(cn.cvking.forge.metadata.annotation.Field)。

参数默认值用途
relationFields(){}本模型侧关联字段名
referenceFields(){}目标模型侧被关联字段名(缺省取目标主键)
targetModel()Void.class目标模型类(M2O/O2O 可省,按字段类型推断)
throughModel()Void.classM2M 中间表模型类(M2M 必填)
throughRelationFields(){}中间表指向本模型的外键列(M2M 必填)
throughReferenceFields(){}中间表指向目标模型的外键列(M2M 必填)
onDelete()NONE级联删除策略(占位,未实现)
onUpdate()NONE级联更新策略(占位,未实现)
java
// M2O:本表外键 categoryId 指向 Category 主键
@Field.M2O
@Field.Relation(relationFields = "categoryId")
@EqualsAndHashCode.Exclude @ToString.Exclude
private Category category;

// O2M:对端 Pet 的 ownerId 指向本表
@Field.O2M
@Field.Relation(referenceFields = "ownerId")
@EqualsAndHashCode.Exclude @ToString.Exclude
private List<Pet> pets;

// M2M:通过 PetTag 中间表,本端 petId、目标端 tagId
@Field.M2M
@Field.Relation(throughModel = PetTag.class,
                throughRelationFields = "petId",
                throughReferenceFields = "tagId")
@EqualsAndHashCode.Exclude @ToString.Exclude
private List<Tag> tags;

TIP

双向关系字段务必加 @EqualsAndHashCode.Exclude@ToString.Exclude,否则填充后互相引用会触发 StackOverflowError

FieldType 字段业务类型

字段类型由 @Field.String / @Field.Long / @Field.Enum 等子注解声明,落到 FieldDefinition.fieldType。关系类字段的 isRelation()true,无数据库列(建表时列类型留空)。

子注解关键参数用途一句话
@Field.Stringlength(默认 255)变长字符串
@Field.Text长文本 TEXT
@Field.LongText超长文本 LONGTEXT
@Field.Integer整型
@Field.Long长整型
@Field.Boolean布尔
@Field.BigDecimalprecision(默认 19)、scale(默认 4)精确小数(金额)
@Field.Datetype(DATE/DATETIME/TIMESTAMP/TIME,默认 DATETIME)日期时间
@Field.EnummultiSerialize(默认 COMMA)、length(默认 50)枚举(须实现 ValueEnum
@Field.O2O / @Field.M2O / @Field.O2M / @Field.M2M@Field.Relation关系字段(虚拟,无列)
java
@Field(displayName = "售价")
@Field.BigDecimal(precision = 10, scale = 2)
private BigDecimal price;

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

CascadeType 级联策略

@Field.RelationonDelete / onUpdate 取值(cn.cvking.forge.metadata.annotation.CascadeType)。当前版本仅声明,行为未实现,可视为占位。

枚举值含义(占位语义)
NONE不做级联(默认)
SET_NULL置空外键
CASCADE级联到关联记录
RESTRICT存在关联时阻止操作

ValueEnum 枚举契约

所有需落库的枚举字段必须实现 ValueEnum<T>cn.cvking.forge.metadata.annotation.ValueEnum),普通 Java 枚举不允许落库,否则启动期校验失败。存储标量类型 EnumStoreType 从泛型实参推断(ValueEnum<Integer> → INT,ValueEnum<String> → STRING,ValueEnum<Long> → LONG),不再由注解声明。

方法签名用途
T getValue()入库标量值
String getDisplayName()显示名称(前端展示,必须提供)
String getHelper()帮助说明(可选,缺省空串)
java
@Getter
@AllArgsConstructor
public enum PetStatus implements ValueEnum<Integer> {

    ON_SALE(1, "在售"),
    SOLD(2, "已售"),
    OFF_SHELF(3, "下架");

    private final Integer value;
    private final String displayName;

    @Override
    public String getHelper() {
        return "";
    }
}
MultiSerialize 多选序列化方式

当枚举字段为多选时(集合形态),@Field.EnummultiSerialize 决定存储格式:

取值存储形态
COMMA逗号分隔字符串(默认)
JSONJSON 数组
BITMASK位运算掩码

常见反例与排查

反例 1:枚举未实现 ValueEnum 直接落库

现象:启动期校验失败抛异常,应用无法起来。 原因:持久化枚举字段必须实现 ValueEnum<T>,普通 Java 枚举不允许落库。 修复:让枚举 implements ValueEnum<Integer>(或 String/Long),并实现 getValue / getDisplayName / getHelper,存储类型由泛型自动推断。

反例 2:M2M 放裸 id 集合保存

现象:Models.saveWith(pet, "tags") 行为不符合预期或抛异常。 原因:M2M 集合元素必须是目标对象或仅含被引用字段/主键的「半对象」,不支持裸主键值集合(如 Set<Long>),中间表无外键标量列可兜底。 修复:把 id 集合先包装为对象集合(每个对象只填被引用字段/主键)再保存。

反例 3:双向关系字段触发 StackOverflowError

现象:填充后调用 toString()equals() 栈溢出。 原因:Pet.ownerOwner.pets 互相引用,Lombok 生成的 toString/equals 递归。 修复:双向关系字段统一加 @EqualsAndHashCode.Exclude@ToString.Exclude

反例 4:M2M 同步时主键为空

现象:saveWith 维护 M2M 时抛「M2M 需主键非空」异常。 原因:syncM2M 在本表落库后执行,依赖已分配的主键去维护中间表;若实体未先落库或主键被清空则无法定位关联。 修复:使用 Models.saveWith 让其按「回填外键 → 落本表分配主键 → 同步中间表」的顺序自动编排,不要手动绕过本表保存直接同步 M2M。