EggJS tegg 事务注解:@eggjs/transaction-decorator 的传播机制与数据源配置实战
2026/9/21 19:41:26 网站建设 项目流程

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_NEWPropagationType.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(类型与常量)、builderdecoratorutil,外部只需一条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)

当应用存在多个数据源(例如订单库、用户库分库部署)时,需要明确每个事务方法绑定哪个数据源。@TransactionaldatasourceName参数正是为此设计,原文档给出了简洁示例:

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):

  1. 默认数据源规则datasourceName未指定时,默认使用module(模块)对应的数据源;非 module 场景下则使用default数据源。
  2. 数据源连接相互隔离:不同数据源之间的连接是隔离的,各自的回滚也是独立的。
  3. 跨数据源不联动回滚:例如函数 B(绑定数据源 B)在函数 A(绑定数据源 A)中执行,当 A 执行异常时,不会回滚 B 中已经执行的 SQL

这三点意味着:多数据源事务本质上是一种"局部事务"方案,每个数据源维护各自独立的事务上下文,跨库的强一致提交/回滚需要应用层自行协调(如补偿或分布式事务方案),注解本身不会替你承担跨数据源的两阶段提交。

在测试夹具 test/fixtures/transaction.ts 中可以看到数据源参数的真实组合用法:

export class Foo { @Transactional() async defaultPropagation(msg: string): Promise<void> { console.log('msg: ', msg); } @Transactional({ datasourceName: 'testDatasourceName1', }) async requiredPropagation(msg: string): Promise<void> { console.log('msg: ', msg); } @Transactional({ propagation: PropagationType.ALWAYS_NEW }) async alwaysNewPropagation(msg: string): Promise<void> { console.log('msg: ', msg); } } export class Bar { @Transactional({ datasourceName: 'datasourceName2' }) async foo(msg: string): Promise<void> { console.log('msg: ', msg); } @Transactional({ propagation: PropagationType.ALWAYS_NEW }) async bar(msg: string): Promise<void> { console.log('msg: ', msg); } }

综合使用示例:传播 + 数据源叠加

propagationdatasourceName是相互独立的维度,可以自由组合。结合源码与测试夹具,一个综合示例可以这样组织:

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, }); };
  1. 通过setIsTransactionClazz类级别打上"这是一个事务类"的标记(IS_TRANSACTION_CLAZZ);
  2. 通过addTransactionMetadata方法级别的事务信息(传播类型、方法名、数据源名)追加到该类的元数据列表中(TRANSACTION_META_DATA)。

之所以要同时维护"类标记"和"方法元数据列表",是为了让运行时可以快速判断"这个类是否需要事务处理",再按需读取具体方法的事务配置。

元数据工具:基于 Symbol 的存储

TransactionMetadataUtil.ts 封装了对元数据的全部读写操作,底层复用了@eggjs/core-decoratorMetadataUtil

  • setIsTransactionClazz/isTransactionClazz:写读类级事务标记;
  • addTransactionMetadata:使用initOwnArrayMetaData初始化(或复用)数组并追加一条方法元数据;
  • getTransactionMetadataList:读取某个类的全部事务方法元数据数组。

对应的存储键是全局注册的 Symbol(见 tegg/core/types/src/transaction.ts):

export 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,印证了两种传播类型在运行时侧有独立的处理路径)。

类型模型:元数据的形状

TransactionMetadataTransactionalParams的定义集中在 tegg/core/types/src/transaction.ts:

export 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 断言了关键行为:
    • 带注解的类FooBarFooBar均被正确标记为事务类,build()输出的元数据数组与期望值逐字段一致(含methodpropagationdatasourceName);
    • 无注解的BarFoo不会被标记为事务类,build()返回空数组[]
    • 非法传播类型在装饰器应用阶段即抛错。
  • 快照测试index.test.ts 与snapshots/index.test.ts.snap 通过toMatchSnapshot固定了模块对外导出的稳定面(PropagationTypeTransactionalTransactionMetaBuilderTransactionMetadataUtil及相关 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询