搜索 K
Appearance
Appearance
在宠物商店里,下架一只宠物、清退一个标签、甚至下线一张中间表,都不应该让数据「凭空消失」。Forge 默认走安全侧:行级删除写时间戳归档,列与表级删除做重命名归档,只有显式开启开关才会真正物理删除。本篇给出可直接复制的实体写法、application.yml 配置项,以及启动与调用时控制台打印的代表性日志,帮助你确认行为符合预期。
TIP
本篇只讲「怎么用、怎么验」。逻辑删除拦截器如何改写 SQL、删表归档链路的内部编排,见设计篇 逻辑删除与删表删列设计。
Forge 的逻辑删除标记列默认叫 deleted,它存的不是 0/1 布尔,而是删除时间戳:
deleted 为 0(或等价的「未删」状态)。deleted 写入删除发生的时间戳。这样设计的直接收益是唯一索引可复用:一只宠物软删后,它原本占用的唯一值能被新数据再次使用。详见下文「唯一索引含 deleted 列」。
继承 IdModel<T> 的 STORE 模型默认跟随全局逻辑删除配置。以宠物主角为例:
@Data
@EqualsAndHashCode(callSuper = true)
@Model(name = "pet.Pet", displayName = "宠物")
public class Pet extends IdModel<Pet> {
@Field(displayName = "名称")
@Field.String(length = 64)
private String name;
@Field(displayName = "状态")
@Field.Enum
private PetStatus status;
@Field(displayName = "售价")
@Field.BigDecimal(precision = 10, scale = 2)
private BigDecimal price;
@Field(displayName = "分类ID")
@Field.Long
private Long categoryId;
}不需要写任何 deleted 字段,逻辑删除列由框架统一注入与维护。
如需对单个模型覆盖全局行为,用 @Model.Advanced:
// 强制启用逻辑删除,并自定义标记列名
@Model(name = "pet.Pet", displayName = "宠物")
@Model.Advanced(logicDelete = LogicDelete.ENABLED, logicDeleteColumn = "deleted_at")
public class Pet extends IdModel<Pet> {
// ...
}logicDelete 是三态枚举,三者语义如下:
| 取值 | 含义 |
|---|---|
DEFAULT | 跟随全局配置 forge.model.logic-delete.enabled(默认值) |
ENABLED | 该模型强制启用逻辑删除 |
DISABLED | 该模型强制关闭逻辑删除(DELETE 即物理删行) |
logicDeleteColumn 为空时取全局配置 forge.model.logic-delete.column,全局默认 deleted。
forge:
model:
logic-delete:
enabled: true # 全局逻辑删除开关(默认开启)
column: deleted # 全局逻辑删除标记列名业务侧的删除调用方式不变,逻辑删除由拦截器在底层透明改写:
// 下架并删除一只宠物(启用逻辑删除时,底层改写为 UPDATE)
Models.origin(Pet.class).deleteById(petId);
// 后续查询自动过滤已软删行,无需手写 deleted 条件
List<Pet> onSale = Models.origin(Pet.class)
.where(w -> w.eq(Pet::getStatus, PetStatus.ON_SALE))
.queryList();在 application.yml 开启 SQL 与框架 debug 日志:
logging:
level:
cn.cvking.forge: debug
# MyBatis-Plus 执行的 SQL(mapper 包按实际工程调整)
com.baomidou.mybatisplus: debug启用逻辑删除时,deleteById 在底层不是 DELETE,而是被 LogicDeleteInnerInterceptor 改写为 UPDATE,且后续 SELECT 自动追加未删过滤条件,控制台可见类似:
-- 删除被改写为更新(写入删除时间戳)
UPDATE pet SET deleted = ? WHERE id = ? AND deleted = 0;
-- 查询自动追加未删过滤
SELECT * FROM pet WHERE status = ? AND deleted = 0;TIP
看到 DELETE 改写成 UPDATE、SELECT 末尾自动带上 deleted 条件,就说明逻辑删除已生效。若想确认是哪个模型在生效,可对照下一节启动期落库的 logicDelete / logicDeleteColumn 元数据。
行级逻辑删除最容易踩的坑就是「软删后唯一值无法复用」。Forge 在模型启用逻辑删除时,会让唯一索引在末尾自动追加逻辑删除列,使唯一性约束变成「未删数据内唯一」。
以 Category 的 code 唯一约束为例:
@Model(name = "pet.Category", displayName = "分类")
@Model.Advanced(unique = "code")
public class Category extends IdModel<Category> {
@Field(displayName = "名称")
@Field.String(length = 64)
private String name;
@Field(displayName = "编码")
@Field.String(length = 64)
private String code;
}启用逻辑删除后,框架生成的唯一索引生效列不是单独的 code,而是 (code, deleted)。语义上:同一个 code,未删行至多一条;当那条被软删(deleted 写入时间戳)后,新行可以再次使用同一个 code,二者 deleted 不同因而不冲突。
WARNING
这正是 deleted 必须用「时间戳」而非 0/1 的根因。若用 0/1,多条软删行的 deleted 都等于 1,(code, 1) 会互相撞唯一索引,软删一旦发生第二次就会抛唯一键冲突。详见文末反例。
当你从模型里删掉一个字段、或整类删掉一个模型,启动期 DDL 链路不会贸然 DROP。默认走逻辑删除:
_d_ 前缀的归档列,保留数据。_d_{原表名}_{时间戳} 的归档表,保留数据。是否物理删除由两个开关控制,二者默认都为 false(安全侧):
forge:
ddl:
allow-drop: false # 删列开关:false=逻辑删除(重命名 _d_ 归档列);true=物理 DROP COLUMN
allow-drop-index: false # 删/重建索引开关:false=拦截保留;true=物理 DROP/REBUILD INDEX对应的属性类是 DdlProperties,allowDrop 与 allowDropIndex 字段默认值均为 false。
归档表名带 _d_ 前缀与时间戳后缀,即 _d_{原表名}_{时间戳}。框架在检测「消失表」时会主动跳过已带 _d_ 前缀的表,因此归档表不会被二次归档或误删。
启动期 DDL 链路会打印 lifecycle 与删表归档日志。先开启日志:
logging:
level:
cn.cvking.forge: debug默认(allow-drop: false)下线一张表,控制台可见归档日志:
[DDL] 逻辑删表,归档为 _d_pet_tag_1717459200000
Lifecycle=DDL, 模块 2 个, 扫描模型 7 个, 耗时 138ms把开关改为 allow-drop: true 后再启动,则是物理删表日志:
[DDL] 物理删表 DROP TABLE pet_tag(allow-drop=true)TIP
分辨二者很简单:日志出现「归档为 _d_...」就是安全侧的逻辑删;出现「物理删表 DROP TABLE」就是真的删了。生产环境务必让 allow-drop 保持 false。
启动期还会打印 lifecycle 概览与模型扫描数量,可用来确认本轮扫描到的模型集合是否符合预期:
[lifecycle] Module topology order: [base, pet]
Lifecycle=DDL, 模块 2 个, 扫描模型 7 个, 耗时 138ms若以 dry-run 方式运行 DDL,链路结束后会把变更脚本写入文件并退出 JVM,日志形如:[DDL] Lifecycle=DDL completed, dry-run file: /path/to/xxx.sql, JVM will exit now.。此时不会真正改库,适合上线前预审。
反例 1:把 deleted 当 0/1,软删第二次撞唯一索引
现象:第一只 code=CAT-001 的分类软删后,新建同 code 正常;再把新的也软删,启动或写入时报唯一键冲突。
原因:把 deleted 当作 0/1 布尔使用时,所有软删行的 deleted 都等于 1。唯一索引生效列是 (code, deleted),多条 (CAT-001, 1) 互相冲突。
修复:不要自定义 0/1 的 deleted 列,沿用框架默认的时间戳语义——未删为 0、已删写删除时间戳。每条软删行时间戳不同,(code, 时间戳) 自然不冲突,唯一值得以复用。
反例 2:生产环境把 allow-drop 设成 true,删字段即丢数据
现象:调整模型字段后启动,控制台打印 [DDL] 物理删表 DROP TABLE ...,旧列旧表数据彻底消失,无法回滚。
原因:forge.ddl.allow-drop 被显式设为 true,删列删表走物理 DROP,不再 _d_ 归档。
修复:生产环境保持 allow-drop: false(这也是默认值)。需要物理回收 _d_ 归档表/列时,在确认归档数据无用后由 DBA 单独人工执行,而不是把全局开关常开。
反例 3:TRANSIENT/非 STORE 模型不参与建表,别误以为「消失表」
现象:把某模型从 STORE 改成 TRANSIENT(或本就是 TRANSIENT/ABSTRACT/PROXY),启动后发现库里相关表没了,怀疑被误删。
原因:只有 STORE 模型才有数据库表。SysModel 的 tableName 对非 STORE 模型为 null,它们本就不参与建表。若一张存量物理表对应的模型不再是 STORE,启动期会把它视为「消失表」按归档规则处理。
修复:确认模型类型是否真的需要落库;需要保留表就维持 ModelTypeEnum.STORE。即便被当作消失表,默认 allow-drop: false 也只是归档为 _d_ 前缀表,数据仍在,重命名回来即可恢复。
反例 4:删除不生效或全表数据「凭空过滤」
现象:调用 deleteById 后行真的没了(物理删),或查询结果莫名少了很多行。
原因:模型 @Model.Advanced(logicDelete = LogicDelete.DISABLED) 关闭了逻辑删除(物理删),或全局 forge.model.logic-delete.column 与实际表里的删除列名不一致,导致拦截器追加的过滤条件命中了非预期列。
修复:核对模型的 logicDelete 三态与全局 enabled;确保 logicDeleteColumn / 全局 column 与表结构一致。可对照启动期落库的 SysModel.logicDelete 与 SysModel.logicDeleteColumn 元数据确认生效值。