程式碼走讀:一個 getter 如何讓 repository 自動加入交易
交易傳染那篇講了概念,這篇看實作——整個機制的魔法濃縮在一個 getter 裡。
消費端長什麼樣
repository 想參與跨模組交易,只要改繼承、constructor 加兩個參數:
typeorm-container.repository.tsts
@Injectable()
export class TypeOrmContainerRepository
extends BaseTypeOrmRepository<ContainerOrmEntity>
implements IContainerRepository
{
constructor(
@InjectRepository(ContainerOrmEntity) defaultRepo: Repository<ContainerOrmEntity>,
txRunner: TransactionRunner,
) {
super(ContainerOrmEntity, defaultRepo, txRunner);
}
// 方法本體照舊用 this.repo.find(...) / this.repo.save(...)——零改動
}
注意方法本體完全不用改。祕密在 base class 把 this.repo 做成 getter:
base-typeorm.repository.ts(核心示意)ts
protected get repo(): Repository<T> {
const manager = this.txRunner.currentManager; // 讀 AsyncLocalStorage
return manager ? manager.getRepository(this.entity) : this.defaultRepo;
}
每次取用當下判斷:目前這條 async 呼叫鏈上有沒有進行中的交易?有,回傳綁定交易的 repository;沒有,回傳 DI 注入的預設版。currentManager 背後就是 AsyncLocalStorage.getStore()——Node 把值綁在呼叫鏈上,跨越任意層的 await 都取得到。
為什麼是 getter 而不是欄位
欄位在 constructor 執行時就定案了,但「是否在交易中」是呼叫當下才知道的事。同一個 repository 實例(NestJS 的 provider 是單例)會同時服務交易內與交易外的呼叫,狀態不能存在實例上——這也是為什麼整個設計借 AsyncLocalStorage 存「每條呼叫鏈自己的狀態」,而不是類別屬性。
刻意的 opt-in
不是所有 repository 都該繼承這個 base——沒有跨模組交易需求的模組,多一層機制就是多一層認知負擔。遷移成本刻意壓到很低(六行 constructor、方法零改動),所以「等真的需要再改」是便宜的——架構裡最好的預留,是讓未來的改動便宜,而不是預先把改動做完。
讀到這裡若有一點收穫,替這顆種子澆一次水——匿名、一人一次。