[Design] Transaction for Repository

Cyan Ho
8 min readJan 20, 2020
Photo by AbsolutVision on Unsplash

最近在研究 DDD(Domain-Driven Design),在研究的過程中,忽然頓悟了一個之前苦惱許久的問題,雖然跟 DDD 沒有特別相關,但這種莫名奇妙的觸類旁通覺得很有趣,讓我想特別記錄下來。

問題主要是關於如何讓 Service 有能力決定何時該執行 Transaction(後面簡稱 TX),並且讓 Repository 的多個操作能夠在 Service 發起的 TX 下執行。

為了方便測試開發以及程式的可讀性,通常在 Repository 上的操作方法,一個方法只會做單一一件事,例如:

ABRepository
createA(dataA)
createB(dataB)

(原諒我偷懶,例子都是示意用的 pseudo code 🥰)

假設有一個資源集合叫 AB,他對應的 Repository 叫 ABRepository,我們可以為 AB 新增資源 A 與 B,分別對應於 Repository 上的 createA 與 createB 方法。

如果 A 與 B 總是分開建立,真的很棒。但如果有一天 A 與 B 必須在同個 TX 裡建立,該怎麼處理呢?

有幾種方法可以解決:

1. 在 Repository 上新增 createAB 方法

ABRepository
createAB(dataA, dataB)
createA(dataA)
createB(dataB)

這是最土炮的方式,對於每個需要 TX 的操作,讓他變成 Repository 上的 atomic operation,如 ABRepository 中的 createAB 方法所示,用起來會長這樣:

Service
createAB(dataA, dataB)
this.abRepository.createAB(dataA, dataB)

但是這樣做有三個缺點:

  1. 當一個 TX 裡需要做很多事時,這個 atomic operation 會變得很肥。
  2. 事務(TX)邏輯是 Service 的職責,若以 atomic operation 的方式處理,會讓事務邏輯都被封裝到了 Repository,Service 反而變得無所事事,使得 Service 的程式碼無法揭露重要的應用程式邏輯。
  3. 無法支援多 Repository 事務。

如果只是把簡單的複合操作(例如這裡的 createAB)封裝成 atomic operation 並不會造成太嚴重的後果,但是我還是會建議盡量避免這個方法,讓 Service 能夠揭露越多邏輯越好。

2. 在 Repository 上新增 begin 與 commit 方法

ABRepository
begin()
commit()
createA(dataA)
createB(dataB)

當 Repository 的 begin 方法被呼叫後,Repository 內部會初始化一個 TX,在遇到 commit 之前的所有操作,都會加進這個 TX。但這個方法會使得 Repository 變成有狀態的,如果 Repository 本身是 Singleton,就不能使用這個方法。

在 Service 中使用起來會像這樣:

Service    
createAB(dataA, dataB)
this.abRepository.begin()
this.abRepository.createA(dataA)
this.abRepository.createB(dataB)
this.abRepository.commit()

對於單個 Repository 的情境,這樣的解法是可行的。如果是多個 Repository,就會有點彆扭:

Repository
begin()
commit()
ARepository extends Repository
createA(dataA)
BRepository extends Repository
createB(dataB)
Service
createAB(dataA, dataB)
this.aRepository.begin()
this.bRepository.begin()

this.aRepository.createA(dataA)
this.bRepository.createB(dataB)

this.aRepository.commit()
this.bRepository.commit()

很顯然的,這兩個 Repository 會自掃門前雪,各自擁護自己的 TX,沒有機制可以互相分享。

3. 讓 Repository 的 begin 返回 TX

ABRepository
begin() TX
createA(dataA)
createB(dataB)

如果讓 Repository 的 begin 方法能夠返回 TX,就能讓大家都存取到同一個 TX,只是必須想個方法將 TX 傳給 Repository,最直覺的方式就是透過參數:

ABRepository
begin() TX
createA(tx, dataA)
createB(tx, dataB)

單個 Repository 使用起來會像這樣:

Service
createAB(dataA, dataB)
tx = this.abRepository.begin()
this.abRepository.createA(tx, dataA)
this.abRepository.createB(tx, dataB)
tx.commit()

要注意的是,這個方法必須在支援 TX 的方法上加入 TX 參數,該方法如果有時候不需使用到 TX,會造成一些使用上的麻煩。

或許可以在 Repository 上提供設置以及清除 TX 的方法:

ABRepository
setContext(tx)
removeContext()
begin() TX
createA(dataA)
createB(dataB)

使用起來會像這樣:

Service
createAB(dataA, dataB)
tx = this.abRepository.begin()

this.abRepository.setContext(tx)

this.abRepository.createA(dataA)
this.abRepository.createB(dataB)

tx.commit()

this.abRepository.removeContext()

要注意的是,這個方法會使得 Repository 變成有狀態的,如果 Repository 本身是 Singleton,就不能使用這個方法,只能選擇上面傳入 TX 參數的方式。

不管是傳入或是設置 TX 都可以有效地解決跨 Repository 的問題,以設置 TX 的方式為例:

Repository
setContext(tx)
removeContext()
begin() TX
ARepository extends Repository
createA(dataA)
BRepository extends Repository
createB(dataB)
Service
createAB(dataA, dataB)
tx = this.aRepository.begin()
// or tx = this.bRepository.begin()


this.aRepository.setContext(tx)
this.bRepository.setContext(tx)

this.aRepository.createA(dataA)
this.bRepository.createB(dataB)

tx.commit()

this.aRepository.removeContext()
this.bRepository.removeContext()

從上面這個例子可以發現一個令人困惑的地方:A 和 B Repository 都有能力可以發起 TX,所以該由誰來發起呢?其實誰來發起都可以,但讀起來和用起來不是特別舒服 😅

4. 引入獨立的 TXProvider

TXProvider
begin() TX

藉由引入一個獨立的 TXProvider,可以讓職責更明確,這邊只列出多個 Repository 的情境:

Repository
setContext(tx)
removeContext()
ARepository extends Repository
createA(dataA)
BRepository extends Repository
createB(dataB)
Service
createAB(dataA, dataB)
tx = this.txProvider.begin()

this.aRepository.setContext(tx)
this.bRepository.setContext(tx)

this.aRepository.createA(dataA)
this.bRepository.createB(dataB)

tx.commit()

this.aRepository.removeContext()
this.bRepository.removeContext()

上述例子中的 Repository 與 TXProvider 都被視為介面,其對應的實例會以某種方式注入到 Service 裡面,實作細節因不同語言、情境而異,在這邊就不多著墨。

--

--