EggJS tegg 事务注解:@eggjs/transaction-decorator 的传播机制与数据源配置实战

发布时间:2026/9/21 19:41:40
EggJS tegg 事务注解:@eggjs/transaction-decorator 的传播机制与数据源配置实战 EggJS tegg 事务注解eggjs/transaction-decorator 的传播机制与数据源配置实战【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址: https://gitcode.com/gh_mirrors/eg/egg导读eggjs/transaction-decorator是 EggJS tegg 框架体系中负责声明式事务的注解模块它允许开发者通过Transactional装饰器以极简的 TypeScript 语法为业务方法声明事务边界从而将事务开启、传播、提交与回滚的控制权从业务代码中彻底解耦。读完本文你将掌握PropagationType.ALWAYS_NEW与PropagationType.REQUIRED两种传播语义的区别与选择场景、datasourceName多数据源事务的隔离特性并能从源码层面理解装饰器如何把注解转化为可被运行时消费的元数据。模块定位一个纯注解层的声明式事务方案在 tegg 框架中eggjs/transaction-decorator位于 tegg/core/transaction-decorator从 package.json 可以确认它是一个独立的、专注于事务注解的包description: tegg transaction decorator其职责被刻意收敛为两层注解定义层对外暴露Transactional装饰器供开发者在业务类方法上声明事务需求元数据构建层把注解参数转换为标准化的TransactionMetadata结构交由 tegg 运行时runtime消费最终驱动实际的事务管理器执行。它仅依赖eggjs/core-decorator提供底层MetadataUtil元数据工具与eggjs/tegg-types提供类型与常量定义自身不直接持有数据库连接或事务管理器这也意味着它可以在任何 tegg 风格的模块中独立复用而具体的事务实现如基于某个 ORM 的事务封装由上层运行时注入。从 src/index.ts 可以看到模块入口将四部分内容一并导出eggjs/tegg-types/transaction类型与常量、builder、decorator与util外部只需一条import语句即可拿到全部能力。核心概念一事务传播机制PropagationType两种传播类型的语义传播机制解决的是当前方法执行时调用栈上已经存在一个事务该如何处理的问题。PropagationType定义在 tegg/core/types/src/transaction.ts共有两个取值取值语义典型场景PropagationType.ALWAYS_NEW不管当前调用栈是否存在事务始终让当前函数在一个全新的事务中执行需要保证某个操作无论如何都独立提交/回滚不随外层事务成败而改变PropagationType.REQUIRED如果当前调用栈存在事务则直接复用否则创建一个新事务默认值最常见的事务语义内层方法自然并入外层事务共享同一次提交/回滚REQUIRED是Transactional的默认传播方式。这一点在装饰器实现 Transactional.ts 中写得很直白const propagation params?.propagation || PropagationType.REQUIRED;即不传propagation时自动采用REQUIRED。传播机制的代码示例原文档给出了传播机制的典型用法Foo.bar声明为ALWAYS_NEWFoo.foo声明为REQUIRED调用bar()时import { PropagationType, Transactional } from eggjs/transaction-decorator; export class Foo { Transactional({ propagation: PropagationType.ALWAYS_NEW }) async bar() { // 这里始终运行在一个全新的事务中 await this.foo(); } Transactional({ propagation: PropagationType.REQUIRED }) async foo(msg) { console.log(has msg: , msg); } }关键结论正如原文档所述Foo.bar始终会在一个独立的事务中执行而Foo.foo会在Foo.bar的事务中执行。原因是foo采用REQUIRED当它被bar调用时检测到调用栈上已存在bar开启的事务因此直接复用两个方法共享同一次事务提交或回滚而bar本身采用ALWAYS_NEW无论外层是否已有事务它都会强制开启新事务从而形成独立的事务边界。非法传播类型的运行时保护值得注意的一个细节装饰器在参数解析阶段就会校验传播类型是否合法。在 Transactional.ts 中有如下逻辑if (!Object.values(PropagationType).includes(propagation)) { throw new Error(unknown propagation type ${propagation}); }也就是说如果误传了xx之类的非法字符串在装饰器应用阶段即类定义加载时就会立即抛出unknown propagation type xx而不是等到运行时才暴露问题。这一行为在 TransactionMetaBuilder.test.ts 中有对应的测试断言覆盖assert.throws(() { Transactional({ propagation: xx as PropagationType }); }, /unknown propagation type xx/);核心概念二多数据源datasourceName当应用存在多个数据源例如订单库、用户库分库部署时需要明确每个事务方法绑定哪个数据源。Transactional的datasourceName参数正是为此设计原文档给出了简洁示例export class Bar { Transactional({ dataSourceName: xx }) async bar() { await this.foo(); } }需要注意的是装饰器内部实际读取的参数名是datasourceName见TransactionalParams类型定义与 Transactional.ts 中的const datasourceName params?.datasourceName;示例中的dataSourceName为笔误形式实际编码时应使用datasourceName键名。关于数据源类型定义中有三句非常重要的约束见 tegg/core/types/src/transaction.ts默认数据源规则datasourceName未指定时默认使用module模块对应的数据源非 module 场景下则使用default数据源。数据源连接相互隔离不同数据源之间的连接是隔离的各自的回滚也是独立的。跨数据源不联动回滚例如函数 B绑定数据源 B在函数 A绑定数据源 A中执行当 A 执行异常时不会回滚 B 中已经执行的 SQL。这三点意味着多数据源事务本质上是一种局部事务方案每个数据源维护各自独立的事务上下文跨库的强一致提交/回滚需要应用层自行协调如补偿或分布式事务方案注解本身不会替你承担跨数据源的两阶段提交。在测试夹具 test/fixtures/transaction.ts 中可以看到数据源参数的真实组合用法export class Foo { Transactional() async defaultPropagation(msg: string): Promisevoid { console.log(msg: , msg); } Transactional({ datasourceName: testDatasourceName1, }) async requiredPropagation(msg: string): Promisevoid { console.log(msg: , msg); } Transactional({ propagation: PropagationType.ALWAYS_NEW }) async alwaysNewPropagation(msg: string): Promisevoid { console.log(msg: , msg); } } export class Bar { Transactional({ datasourceName: datasourceName2 }) async foo(msg: string): Promisevoid { console.log(msg: , msg); } Transactional({ propagation: PropagationType.ALWAYS_NEW }) async bar(msg: string): Promisevoid { console.log(msg: , msg); } }综合使用示例传播 数据源叠加propagation与datasourceName是相互独立的维度可以自由组合。结合源码与测试夹具一个综合示例可以这样组织import { PropagationType, Transactional } from eggjs/transaction-decorator; export class OrderService { // 默认传播REQUIRED 默认数据源并入调用方事务 Transactional() async createOrder() { // ... } // REQUIRED 指定数据源在指定库的事务中执行若调用栈已有同库事务则复用 Transactional({ propagation: PropagationType.REQUIRED, datasourceName: orderDb }) async deductStock() { // ... } // ALWAYS_NEW 指定数据源无论外层如何都在该数据源上开启全新事务 Transactional({ propagation: PropagationType.ALWAYS_NEW, datasourceName: logDb }) async writeAuditLog() { // ... } }选择建议只写业务主库、希望与调用方保持同生共死使用默认REQUIRED无需显式声明操作必须独立落库、不因外层失败而回滚如审计日志、消息记录使用ALWAYS_NEW需要跨多个物理库分别写数据为每个方法显式声明datasourceName并明确接受跨数据源回滚相互独立的语义。底层实现从注解到元数据的完整链路装饰器标注事务类与方法Transactional的核心实现在 Transactional.ts。当装饰器作用于某个方法时它会做两件事return function (target: any, propertyKey: PropertyKey): void { const constructor: EggProtoImplClass target.constructor; TransactionMetadataUtil.setIsTransactionClazz(constructor); TransactionMetadataUtil.addTransactionMetadata(constructor, { propagation, method: propertyKey, datasourceName, }); };通过setIsTransactionClazz在类级别打上这是一个事务类的标记IS_TRANSACTION_CLAZZ通过addTransactionMetadata把方法级别的事务信息传播类型、方法名、数据源名追加到该类的元数据列表中TRANSACTION_META_DATA。之所以要同时维护类标记和方法元数据列表是为了让运行时可以快速判断这个类是否需要事务处理再按需读取具体方法的事务配置。元数据工具基于 Symbol 的存储TransactionMetadataUtil.ts 封装了对元数据的全部读写操作底层复用了eggjs/core-decorator的MetadataUtilsetIsTransactionClazz/isTransactionClazz写读类级事务标记addTransactionMetadata使用initOwnArrayMetaData初始化或复用数组并追加一条方法元数据getTransactionMetadataList读取某个类的全部事务方法元数据数组。对应的存储键是全局注册的 Symbol见 tegg/core/types/src/transaction.tsexport const TRANSACTION_META_DATA: symbol Symbol.for(EggPrototype#transaction#metaData); export const IS_TRANSACTION_CLAZZ: symbol Symbol.for(EggPrototype#IS_TRANSACTION_CLAZZ);使用Symbol.for注册的目的是保证在同一运行时中无论模块被如何加载例如工作区软链、打包等场景元数据键都能保持唯一一致。元数据构建器面向运行时的稳定输出TransactionMetaBuilder.ts 是注解层与运行时之间的适配器export class TransactionMetaBuilder { private readonly clazz: EggProtoImplClass; constructor(clazz: EggProtoImplClass) { this.clazz clazz; } build(): TransactionMetadata[] { if (!TransactionMetadataUtil.isTransactionClazz(this.clazz)) { return []; } return TransactionMetadataUtil.getTransactionMetadataList(this.clazz); } }它对外只暴露一个build()方法不是事务类则返回空数组是事务类则返回标准化的TransactionMetadata[]。从源码结构看tegg 运行时在实例化 Bean 时即可通过new TransactionMetaBuilder(clazz).build()拿到完整事务元数据再据此为方法生成带事务边界的代理实现在 tegg/core/runtime 中可看到ALWAYS_NEW语义对应的容器实现EggAlwaysNewObjectContainer印证了两种传播类型在运行时侧有独立的处理路径。类型模型元数据的形状TransactionMetadata与TransactionalParams的定义集中在 tegg/core/types/src/transaction.tsexport interface TransactionalParams { /** 事务传播方式默认 REQUIRED */ propagation?: PropagationType; /** 数据源默认使用 module 的数据源非 module 时将使用 default 数据源 */ datasourceName?: string; } export interface TransactionMetadata { propagation: PropagationType; method: PropertyKey; datasourceName?: string; }TransactionalParams是开发者写给注解看的输入TransactionMetadata是运行时读取的结构化输出两者的字段几乎一一对应多出的method字段用于标识该条元数据对应类上的哪个方法确保一个类中多个事务方法可以并存而互不混淆。测试验证元数据构建的正确性测试夹具与测试用例共同保证了注解层的语义稳定夹具test/fixtures/transaction.ts 覆盖了全部参数组合默认传播、指定数据源、ALWAYS_NEW、以及完全没有事务注解的普通类BarFoo。构建测试TransactionMetaBuilder.test.ts 断言了关键行为带注解的类Foo、Bar、FooBar均被正确标记为事务类build()输出的元数据数组与期望值逐字段一致含method、propagation、datasourceName无注解的BarFoo不会被标记为事务类build()返回空数组[]非法传播类型在装饰器应用阶段即抛错。快照测试index.test.ts 与snapshots/index.test.ts.snap 通过toMatchSnapshot固定了模块对外导出的稳定面PropagationType、Transactional、TransactionMetaBuilder、TransactionMetadataUtil及相关 Symbol防止公共 API 被无意破坏。使用前提与限制运行环境当前包声明engines: { node: 22.18.0 }见 package.json使用前请确认 Node.js 版本满足要求纯声明层本包只负责注解与元数据真正的事务开启、提交、回滚由 tegg 运行时结合具体数据源实现完成单独引入本包并不会让方法获得事务能力跨数据源隔离多数据源场景下各数据源事务独立回滚存在分布式一致性的边界需要业务层面自行设计补偿或协调策略方法级粒度元数据以类 方法为最小单位method: PropertyKey事务边界始终是单个方法跨方法的合并事务依赖REQUIRED传播语义在调用栈上的复用而非注解层面的显式分组。小结eggjs/transaction-decorator以一枚轻量的Transactional装饰器把传播机制 数据源选择两个最核心的事务决策点交给了声明式配置REQUIRED让内层方法自然融入外层事务ALWAYS_NEW保证关键操作拥有独立事务边界datasourceName则让事务精确绑定到指定数据库。注解在类加载期即完成参数校验与元数据登记运行期通过TransactionMetaBuilder输出标准化配置——这套声明 - 登记 - 构建 - 消费的链路正是 tegg 框架把复杂事务语义沉淀为可复用基础设施的典型范式。【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址: https://gitcode.com/gh_mirrors/eg/egg创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考