搜索 K
Appearance
Appearance
这是数据建模与关系字段相关 API 的速查表,用于忘记签名时快速回顾。所有示例复用宠物商店实体(Pet、Category、Owner、PetProfile、Tag、PetTag),包名 com.demo.petshop.model,框架注解来自 cn.cvking.forge.*。
需要理解底层机制时,请跳转到对应设计篇:数据访问门面设计、关系字段设计。
TIP
所有写示例默认实体继承自 IdModel<T>(带 id、createTime、updateTime),编码模型继承自 CodeModel<T>(额外带唯一 code)。
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) |
最小示例:
// 查询
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 会校验「集合非空、元素非空、元素类型一致」,混入异类元素会直接抛异常。
由 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() |
// 在售且价格低于 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");由 Models.of(T) 得到。
| 方法签名 | 用途 |
|---|---|
save() 返回 T | 主键存在判库决定 insert/update,并按唯一键兜底 |
insert() 返回 int | 强制插入 |
updateById() 返回 int | 按主键更新 |
Models.of(pet).save(); // 智能保存
Models.of(pet).insert(); // 强制插入由 Models.ofBatch(Collection<T>) 得到。
| 方法签名 | 用途 |
|---|---|
insertBatch() 返回 int | 批量插入 |
updateBatchById() 返回 int | 按主键批量更新 |
saveBatch() 返回 int | 按主键与唯一键分流 insert/update 批量执行 |
Models.ofBatch(pets).saveBatch();读填充把外键还原为关系对象,对应实现 DefaultRelationReadApi。日常通过 Models.fieldQuery / Models.listFieldQuery 触发,下表给出四类关系的填充语义。
| 关系类型 | 填充方法 | 行为一句话 | 生成 SQL 形态 |
|---|---|---|---|
| M2O / O2O | fillToOne | 收外键值,IN 查目标表,按被引用值建索引写回单个对象 | SELECT * FROM target_table WHERE ref_column IN (...) |
| O2M | fillO2M | 收本模型被引用值,IN 查对端表,按对端外键分组写回集合 | SELECT * FROM target_table WHERE ref_column IN (...) |
| M2M | fillM2M | 先 IN 查中间表得目标键,再 IN 查目标表,按映射写回集合 | 中间表 + 目标表两次 IN 查询 |
// 单实体:填充宠物的分类、主人、档案、标签
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");-- 1. 中间表:按宠物主键查关联行
SELECT * FROM pet_tag WHERE pet_id IN (1, 2, 3);
-- 2. 目标表:按收集到的标签键查标签
SELECT * FROM tag WHERE id IN (10, 11, 12);写维护对应实现 DefaultRelationWriteApi,由 Models.saveWith 编排,纯应用层完成,不依赖数据库外键约束。
| 方法 | 关系类型 | 时机 | 行为一句话 |
|---|---|---|---|
applyToOne | M2O / O2O | 本表落库前 | 从关系对象读被引用值,回填本表外键列 |
syncM2M | M2M | 本表落库后 | 按关系集合与中间表现状做 diff,增删中间表行 |
saveWith 的内部顺序:先 applyToOne 回填外键,再 save() 落本表分配主键,最后 syncM2M 同步中间表。
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");两种保存形态与几条硬约束
null 时 fail-fast 抛异常。Set<Long>)。关系字段用 @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.class | M2M 中间表模型类(M2M 必填) |
throughRelationFields() | {} | 中间表指向本模型的外键列(M2M 必填) |
throughReferenceFields() | {} | 中间表指向目标模型的外键列(M2M 必填) |
onDelete() | NONE | 级联删除策略(占位,未实现) |
onUpdate() | NONE | 级联更新策略(占位,未实现) |
// 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。
字段类型由 @Field.String / @Field.Long / @Field.Enum 等子注解声明,落到 FieldDefinition.fieldType。关系类字段的 isRelation() 为 true,无数据库列(建表时列类型留空)。
| 子注解 | 关键参数 | 用途一句话 |
|---|---|---|
@Field.String | length(默认 255) | 变长字符串 |
@Field.Text | — | 长文本 TEXT |
@Field.LongText | — | 超长文本 LONGTEXT |
@Field.Integer | — | 整型 |
@Field.Long | — | 长整型 |
@Field.Boolean | — | 布尔 |
@Field.BigDecimal | precision(默认 19)、scale(默认 4) | 精确小数(金额) |
@Field.Date | type(DATE/DATETIME/TIMESTAMP/TIME,默认 DATETIME) | 日期时间 |
@Field.Enum | multiSerialize(默认 COMMA)、length(默认 50) | 枚举(须实现 ValueEnum) |
@Field.O2O / @Field.M2O / @Field.O2M / @Field.M2M | 配 @Field.Relation | 关系字段(虚拟,无列) |
@Field(displayName = "售价")
@Field.BigDecimal(precision = 10, scale = 2)
private BigDecimal price;
@Field(displayName = "状态")
@Field.Enum
private PetStatus status;@Field.Relation 的 onDelete / onUpdate 取值(cn.cvking.forge.metadata.annotation.CascadeType)。当前版本仅声明,行为未实现,可视为占位。
| 枚举值 | 含义(占位语义) |
|---|---|
NONE | 不做级联(默认) |
SET_NULL | 置空外键 |
CASCADE | 级联到关联记录 |
RESTRICT | 存在关联时阻止操作 |
所有需落库的枚举字段必须实现 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() | 帮助说明(可选,缺省空串) |
@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 "";
}
}当枚举字段为多选时(集合形态),@Field.Enum 的 multiSerialize 决定存储格式:
| 取值 | 存储形态 |
|---|---|
COMMA | 逗号分隔字符串(默认) |
JSON | JSON 数组 |
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.owner 与 Owner.pets 互相引用,Lombok 生成的 toString/equals 递归。 修复:双向关系字段统一加 @EqualsAndHashCode.Exclude 与 @ToString.Exclude。
反例 4:M2M 同步时主键为空
现象:saveWith 维护 M2M 时抛「M2M 需主键非空」异常。 原因:syncM2M 在本表落库后执行,依赖已分配的主键去维护中间表;若实体未先落库或主键被清空则无法定位关联。 修复:使用 Models.saveWith 让其按「回填外键 → 落本表分配主键 → 同步中间表」的顺序自动编排,不要手动绕过本表保存直接同步 M2M。