跳到主要内容

ORM 事件特性

本页说明 Geelato Framework ORM 层内置的事件机制:如何选择扩展点、实现监听器、统一注册并验证是否生效。

当前事件能力位于:

  • cn.geelato.core.orm.event

它不是独立的消息总线,也不是 Spring ApplicationEvent 的简单包装,而是 ORM 执行链路内部的轻量事件钩子。

事件机制围绕 Dao 的写操作(save/delete/batchSave/multiSave/multiDelete)以及查询操作(query)展开,提供「前置同步、后置异步、事务感知、可控线程池、优先级」等能力,覆盖审计、校验、缓存、旁路同步、读拦截等场景。

适用场景​

适合通过事件机制处理的需求:

  • 保存前做校验、补字段、拦截非法写入。
  • 保存后写审计日志、刷新缓存、做旁路同步。
  • 删除前阻止误删。
  • 删除后做清理动作。
  • 查询前后做读审计、慢查询统计、缓存预热(查询事件)。
  • 只在事务真正提交后才执行的副作用(事务感知 after 回调)。

以下需求不适合使用事件机制,应改用对应能力:

解决什么问题​

ORM 事件机制的目标,是把“通用 CRUD 执行”和“业务侧附加动作”解耦。

这样开发者不需要每次都侵入 Dao 或复制整套 CRUD 流程,只需要挂接事件监听器即可。

当前有哪些事件​

ORM 事件分为三大类,每类都分成 Before/After 两个时机:

事件类型前置监听器后置监听器说明
保存BeforeSaveEventListenerAfterSaveEventListenerinsert / update
删除BeforeDeleteEventListenerAfterDeleteEventListenerdelete
查询BeforeQueryEventListenerAfterQueryEventListenerquery(读拦截)

此外,保存和删除还提供事务感知后置监听器:

  • TransactionalAfterSaveEventListener:仅在事务提交/回滚后触发
  • TransactionalAfterDeleteEventListener:同上

监听器还可通过函数式接口用 lambda 注册(详见下文“函数式 callback”)。

先决定该用哪一种​

按下面判断选择监听器:

  • 保存前强校验、强约束、强拦截:BeforeSaveEventListener
  • 保存后异步通知、审计、缓存刷新:AfterSaveEventListener
  • 必须保证只在事务提交后才执行的副作用(如 ES 同步、避免读到回滚数据):TransactionalAfterSaveEventListener
  • 删除前防误删、引用校验:BeforeDeleteEventListener
  • 删除后清缓存、删旁路数据、记审计:AfterDeleteEventListener
  • 查询前后读审计、慢查询统计、缓存预热:QueryEventListener(BeforeQueryEventListener / AfterQueryEventListener)

最重要的区分:

  • Before 同步执行,异常会直接阻断主流程(异常是否阻断由监听器实现者决定,框架透传异常、不做全局开关)
  • After 异步执行,异常只记日志,不影响主流程
  • Transactional After 在事务提交/回滚后触发,解决普通 After「异步、事务外、可能读到回滚数据」的问题

触发时机​

事件在 Dao 的写操作与查询链路中触发:

  • Dao.save(...) / Dao.batchSave(...) / Dao.multiSave(...)
  • Dao.delete(...) / Dao.multiDelete(...)
  • Dao.queryList(...) / Dao.queryForMapList(...)(查询事件)

不管是直接调用 Dao,还是通过 ORM Fluent DSL(MetaFactory.insert/update/delete/query)最终落到这些执行路径,只要进入这些 ORM 入口,就会命中事件机制。

执行模型​

前置事件(Before)​

Before 事件同步执行:

  • 在真正执行 SQL 之前触发
  • 可以修改上下文中的 BoundSql
  • 可以做校验
  • 可以直接抛异常阻断主流程

如果某个前置监听器抛出异常,当前保存/删除/查询流程会直接失败。

异常契约:框架对 Before 监听器抛出的异常采取透传策略(重新抛出给调用方),不提供全局开关。是否阻断业务由监听器实现者自行决定:

  • 校验类监听器(需要拦截非法写入):直接抛异常,由异常阻断主流程。
  • 旁路类监听器(审计、埋点,不应阻断业务):在方法内部 try/catch 自行吞掉异常。

提示:Before 监听器如果抛出非数据访问异常,Dao.multiSave/multiDelete 会确保事务回滚(不会出现事务悬挂)。

后置事件(After)​

After 事件异步执行,通过线程池调度:

  • 保存事件线程名前缀:save-event-*
  • 删除事件线程名前缀:delete-event-*
  • 查询事件线程名前缀:query-event-*

线程池是可控的(默认:核心线程 4 + 有界队列 1000 + CallerRunsPolicy 背压策略),可通过配置调整(详见下文“线程池配置”)。

After 的语义:

  • 主 SQL 已执行完成
  • 监听器被异步调度
  • 监听器异常只记录日志,不影响主流程

适合做:审计日志、镜像表同步、缓存刷新、非关键链路通知、异步旁路处理。

事务感知后置事件(Transactional After)​

普通 After 是异步、在事务提交前调度,监听器无法保证读到已提交数据(事务可能尚未 commit 或已回滚)。

实现 TransactionalAfterSaveEventListener / TransactionalAfterDeleteEventListener 的监听器:

  • afterCommit(context):仅在事务真正提交后触发;无事务时立即触发(视为已提交)
  • afterRollback(context):事务回滚后触发

框架在 fireAfter 时检测监听器是否实现了事务感知接口,若是则把回调登记到事件上下文,由 EventTransactionSupport 结合 Spring TransactionSynchronizationManager 在提交/回滚点触发。

适合做:ES 同步、强依赖提交数据的旁路处理(消除「读到回滚脏数据」风险)。

注意:事务感知 after 回调在 Dao.save/delete/multiSave/multiDelete 路径完整生效。使用时确保写操作经由 Dao 执行。

事件上下文里有什么​

保存事件上下文 SaveEventContext​

字段类型说明
daoDao允许监听器复用当前 ORM 执行能力
sessionCtxSessionCtx保存链路共享的会话级上下文
entityIdEntity当入口来自实体保存时,可直接拿到实体对象
boundSqlBoundSql可看到或修改本次最终执行的 SQL 与参数
commandSaveCommand实体名、值映射等 ORM 保存命令信息
resultValueMapMap主保存完成后的结果值映射
eventIdString本次事件链路唯一标识
startTimelong事件开始时间
successbooleanSQL 是否执行成功(after 触发前置 true,失败为 false)
exceptionThrowable失败时的异常
affectedRowsint受影响行数

便捷判断(避免解析 SQL):

  • getOperType():操作类型(CommandType.Insert / CommandType.Update)
  • isInsert() / isUpdate():是否新增/更新

事务感知回调注册入口:

  • onCommit(Runnable) / onRollback(Runnable)

删除事件上下文 DeleteEventContext​

字段说明
dao / sessionCtx / boundSql / command / affectedRows / eventId / startTime同保存上下文
success / exception执行结果(同保存上下文)

事务感知回调入口:onCommit(Runnable) / onRollback(Runnable)。

查询事件上下文 QueryEventContext​

字段说明
dao / sessionCtx / boundSql / commandORM 执行能力与会话信息
entityType查询目标实体类型(原生 SQL 查询时可能为 null)
success / exception查询是否成功
rowCount返回行数(after 阶段填充,便于慢查询/大结果集统计)
eventId / startTime链路标识与开始时间

监听器接口怎么理解​

SaveEventListener / DeleteEventListener / QueryEventListener 定义:

  • beforeXxx(context) / afterXxx(context)
  • enabled(context):配置级粗开关,应廉价(只读 properties/常量),判断监听器是否全局启用
  • supports(context):单次事件级细匹配,可查元数据/解析 SQL,判断是否处理本次特定事件
  • getOrder():优先级,值小先执行(默认 0)

需要特别注意:

  • enabled(...) 与 supports(...) 默认返回 false
  • 二者均须为 true 才触发回调
  • 不显式覆写这两个方法,监听器即使注册了也不会生效

enabled 与 supports 的语义约定:

  • enabled:粗粒度,判配置(如 properties.isEnabled()、常量开关),每次触发都求值但应廉价
  • supports:细粒度,判单次事件(如按实体名/表名过滤),可查元数据

监听器如何注册​

当前通过静态管理器注册(全局、进程内、以 JVM 为边界),推荐在 Spring 配置类集中注册:

保存事件:SaveEventManager.registerBefore(...) / registerAfter(...) / registerBeforeIfAbsent(...) / registerAfterIfAbsent(...)

删除事件:DeleteEventManager.registerBefore(...) / registerAfter(...) / registerBeforeIfAbsent(...) / registerAfterIfAbsent(...)

查询事件:QueryEventManager.registerBefore(...) / registerAfter(...) / registerBeforeIfAbsent(...) / registerAfterIfAbsent(...)

注销:unregisterBefore(...) / unregisterAfter(...)(推荐在 @PreDestroy 调用,防热部署/上下文刷新累积泄漏)。

注册按 getOrder() 升序插入,值小先执行。

函数式 callback(推荐用于轻量场景)​

为避免实现空标记接口的样板代码,提供函数式 callback 接口,可用 lambda 注册:

import cn.geelato.core.orm.event.SaveEventManager;

// 等价于实现 BeforeSaveEventListener,但无需写空的 afterSave/enabled/supports
SaveEventManager.registerBeforeCallback((ctx) -> {
// 保存前逻辑
});
// 可指定优先级
SaveEventManager.registerBeforeCallback((ctx) -> { /* ... */ }, 10);

可用的 callback 接口:BeforeSaveCallback / AfterSaveCallback / BeforeDeleteCallback / AfterDeleteCallback(均在 cn.geelato.core.orm.event.callback 包)。

注册入口:SaveEventManager.registerBeforeCallback(...) / registerAfterCallback(...),DeleteEventManager 同名方法。

函数式 callback 与传统监听器接口并存,不互相替代;新代码轻量场景推荐 callback。

线程池配置​

后置事件异步线程池由 geelato-orm 的 OrmEventAutoConfiguration 自动装配,可通过配置调整:

# 关闭事件线程池自动装配(回退到管理器默认池)
geelato.orm.event.enabled=true
# 保存事件线程池
geelato.orm.event.save.pool-size=4
geelato.orm.event.save.queue-capacity=1000
# 删除事件线程池
geelato.orm.event.delete.pool-size=4
geelato.orm.event.delete.queue-capacity=1000

特性:

  • 有界队列 + CallerRunsPolicy(队列满时由提交线程执行,背压、不丢任务、不 OOM)
  • 守护线程,容器销毁时优雅关闭(@PreDestroy 调用 shutdown)
  • 也可通过 SaveEventManager.setExecutor(...) / DeleteEventManager.setExecutor(...) 程序化替换(替换前会优雅关闭旧池)

最短接入步骤​

第 1 步:选一个扩展点​

  • 会影响主写入正确性的逻辑,优先放 Before
  • 只是旁路增强的逻辑,优先放 After
  • 必须等事务提交后才执行的逻辑,用 Transactional After

第 2 步:实现监听器​

“保存前校验客户编码”的最小示例:

public class CustomerBeforeSaveListener implements BeforeSaveEventListener {
@Override
public void beforeSave(SaveEventContext context) {
if ("crm_customer".equalsIgnoreCase(context.getCommand().getEntityName())) {
Object code = context.getCommand().getValueMap().get("code");
if (code == null || String.valueOf(code).isBlank()) {
throw new IllegalArgumentException("客户编码不能为空");
}
}
}

@Override
public void afterSave(SaveEventContext context) {
}

@Override
public boolean supports(SaveEventContext context) {
return context.getCommand() != null;
}

@Override
public boolean enabled(SaveEventContext context) {
return true;
}
}

容易漏掉的两个点:supports(...) 与 enabled(...) 默认都是 false,不覆写则注册了也不执行。

第 3 步:在统一入口注册监听器​

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

@Configuration
public class OrmEventConfiguration {

private final CustomerBeforeSaveListener beforeListener = new CustomerBeforeSaveListener();

@PostConstruct
public void registerListeners() {
SaveEventManager.registerBeforeIfAbsent(beforeListener);
}

@PreDestroy
public void unregisterListeners() {
// 生命周期管理:容器销毁时注销,防热部署泄漏
SaveEventManager.unregisterBefore(beforeListener);
}
}

删除/查询链路改成对应的 DeleteEventManager / QueryEventManager。

第 4 步:实际触发并验证​

String id = MetaFactory.insert("Customer")
.value("name", "Demo")
.value("code", "C001")
.save();

验证时优先看三件事:

  • 监听器方法是否真的被触发
  • supports(...) 与 enabled(...) 是否返回 true
  • 注册入口是否只执行了一次(避免重复注册)

当前内置示例:只读影子表监听器​

保存事件默认注册了 ReadonlyShadowTableListener,把保存 SQL 映射到 *_readonly 影子表。其内部开关默认关闭(READONLY_EVENT_ENABLED = false),作为内置参考实现。

开发者可以怎么做定制​

场景 1:保存前校验(Before)​

public class CustomerBeforeSaveListener implements BeforeSaveEventListener {
@Override
public void beforeSave(SaveEventContext context) {
if ("crm_customer".equalsIgnoreCase(context.getCommand().getEntityName())) {
Object code = context.getCommand().getValueMap().get("code");
if (code == null || String.valueOf(code).isBlank()) {
throw new IllegalArgumentException("客户编码不能为空");
}
}
}
@Override public void afterSave(SaveEventContext context) {}
@Override public boolean supports(SaveEventContext context) { return context.getCommand() != null; }
@Override public boolean enabled(SaveEventContext context) { return true; }
}

适合:必填校验、状态流转校验、写入前参数规范化、必须阻断主流程的约束。

场景 2:保存后异步旁路处理(After)​

public class CustomerAfterSaveListener implements AfterSaveEventListener {
@Override public void beforeSave(SaveEventContext context) {}
@Override
public void afterSave(SaveEventContext context) {
if ("crm_customer".equalsIgnoreCase(context.getCommand().getEntityName())) {
System.out.println("customer changed, eventId=" + context.getEventId()
+ ", success=" + context.isSuccess());
}
}
@Override public boolean supports(SaveEventContext context) { return context.getCommand() != null; }
@Override public boolean enabled(SaveEventContext context) { return true; }
}

适合:审计日志、缓存刷新、旁路通知、搜索索引同步。

场景 3:事务感知后置(Transactional After)​

适合必须等事务提交后才执行的副作用(如 ES 同步,避免读到回滚数据):

public class CustomerEsSyncListener implements TransactionalAfterSaveEventListener {
@Override public void beforeSave(SaveEventContext context) {}
@Override public void afterSave(SaveEventContext context) {} // 老的异步 after,留空

@Override
public void afterCommit(SaveEventContext context) {
// 仅在事务提交后执行,此时数据已落库
if ("crm_customer".equalsIgnoreCase(context.getCommand().getEntityName())) {
System.out.println("customer committed, sync to ES, eventId=" + context.getEventId());
}
}

@Override
public void afterRollback(SaveEventContext context) {
// 事务回滚时触发,可用于清理/告警
System.out.println("customer save rolled back, skip ES sync");
}

@Override public boolean supports(SaveEventContext context) { return context.getCommand() != null; }
@Override public boolean enabled(SaveEventContext context) { return true; }
}

注册方式与普通监听器相同:SaveEventManager.registerAfterIfAbsent(listener)(它仍继承 AfterSaveEventListener)。

场景 4:删除前拦截(Before)​

public class CustomerBeforeDeleteListener implements BeforeDeleteEventListener {
@Override
public void beforeDelete(DeleteEventContext context) {
if ("crm_customer".equalsIgnoreCase(context.getCommand().getEntityName())) {
throw new IllegalStateException("客户数据不允许直接删除,请先走业务注销流程");
}
}
@Override public void afterDelete(DeleteEventContext context) {}
@Override public boolean supports(DeleteEventContext context) { return context.getCommand() != null; }
@Override public boolean enabled(DeleteEventContext context) { return true; }
}

场景 5:查询读审计/慢查询统计(Query)​

public class SlowQueryListener implements AfterQueryEventListener {
@Override public void beforeQuery(QueryEventContext context) {}
@Override
public void afterQuery(QueryEventContext context) {
long cost = System.currentTimeMillis() - context.getStartTime();
if (cost > 500) {
System.out.println("slow query, cost=" + cost + "ms, rows=" + context.getRowCount()
+ ", eventId=" + context.getEventId());
}
}
@Override public boolean supports(QueryEventContext context) { return true; }
@Override public boolean enabled(QueryEventContext context) { return true; }
}

注册:QueryEventManager.registerAfterIfAbsent(listener)。

场景 6:函数式 callback(轻量注册)​

@PostConstruct
public void register() {
// 用 lambda 注册保存前校验,无需写实现类
SaveEventManager.registerBeforeCallback(ctx -> {
if (ctx.isUpdate() && "crm_customer".equalsIgnoreCase(ctx.getCommand().getEntityName())) {
// 更新前校验
}
});
}

推荐接入方式​

推荐把监听器注册收口到一个明确的启动装配位置:

  • Spring 启动类初始化阶段
  • 某个统一的 ORM 配置类
  • 某个基础模块的静态初始化逻辑

建议做法:

  1. 每个监听器单独一个类(或用函数式 callback)
  2. 在统一配置类里声明/注册
  3. 在统一初始化入口里调用 register*IfAbsent(...)
  4. 在 @PreDestroy 里调用 unregister*(生命周期管理)
  5. 不要在业务 Service 方法里临时注册

一步一步的排障顺序​

写好了监听器但“没有生效”,按这个顺序排查:

  1. 看是否真的进入了 ORM 的写/查询链路
  2. 看监听器是否已注册到对应 EventManager
  3. 看 supports(...) 是否返回 true
  4. 看 enabled(...) 是否返回 true
  5. 看逻辑是不是写在 After,而你却期待它阻断主流程
  6. 看是否出现重复注册,导致执行多次
  7. (事务感知 after)确认写操作经由 Dao 执行,且回调在事务提交后才触发

使用注意事项​

1. Before 会阻断主流程​

前置监听器异常会中断主流程。框架对异常透传、不加全局开关:需要拦截就抛异常;不需要阻断就自行 try/catch 吞掉。

2. 普通 After 不适合承载强事务语义​

后置监听器异步执行且在事务提交前调度,不要把“必须和主事务完全一致”的动作只放在普通 After。需要强事务一致请用 TransactionalAfter* 监听器,或显式编排事务。

3. 注意线程与上下文边界​

After 事件在线程池执行,不能想当然依赖当前线程本地变量、Web 请求上下文。需要这些信息应从事件上下文取,或在进入事件前显式复制。

4. 注意重复注册与生命周期​

提供 register*IfAbsent(...) 防重复,但若多处各自 new 实例仍可能重复生效。推荐统一管理实例 + 统一注册 + @PreDestroy 注销(尤其热部署/上下文刷新场景)。

5. 不要把查询规则塞进事件里​

ORM 事件适合写/读操作链路的附加动作。查询自动追加租户/权限过滤、保存自动补字段,应优先用 SPI,不要塞进事件监听器。

6. 插入与更新的区分​

SaveCommand 同时覆盖 insert/update。监听器可用 context.isInsert() / context.isUpdate() / context.getOperType() 区分,无需解析 SQL 字符串。

完整示例工程​

平台在 geelato-hello-example 仓库提供了事件机制的可运行示例工程:

  • geelato-sample-orm-event

它基于最轻量的 geelato-orm(H2 内存库),演示:保存前校验、保存后旁路、事务感知 after、查询慢查询统计、函数式 callback、优先级、执行结果回传。运行方式见该工程 README。

总结​

Geelato Framework ORM 事件机制是一套围绕 Dao 写/查询链路的内置扩展点:

  • 前置事件同步执行,适合校验和拦截(异常透传,是否阻断由实现者决定)
  • 后置事件异步执行,适合通知和旁路处理(可控线程池 + 背压)
  • 事务感知后置事件,适合必须等提交后才执行的强一致副作用
  • 查询事件,适合读审计/慢查询/缓存预热
  • 函数式 callback,轻量注册免样板
  • 优先级、生命周期管理、执行结果回传等增强

它特别适合作为:ORM 层统一扩展机制、多业务模块复用的领域钩子、避免侵入核心 CRUD 的定制入口。

推荐继续阅读​