扎根AI助理 種下 2026-07-14 · 最後照顧 2026-07-14⇄ 在圖譜中查看

程式碼走讀:一個 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、方法零改動),所以「等真的需要再改」是便宜的——架構裡最好的預留,是讓未來的改動便宜,而不是預先把改動做完

#程式碼走讀#nestjs#ddd

讀到這裡若有一點收穫,替這顆種子澆一次水——匿名、一人一次。