在決定離開 Spring Boot 後,要怎麼處理資料庫的交易管理?連線池可以直接使用 HikariCP,SQL builder 可以使用 Sql2o (雖然 Sql2o 有提供 ORM 相關的功能,但我幾乎沒用到),可是交易管理怎麼辦呢?
Sql2o 有提供 Transaction 相關的封裝,但要怎麼跟 repository pattern 搭配?一直帶著代表 transaction 的物件跑,總覺得怪怪的?整個 repository 的介面一點也不 collection like。此外,還有一個問題,誰呼叫beginTransactin()、commit() 和 rollback() 呢?交易範圍
要回答誰呼叫 beginTransaction(),其實就是回答誰管理交易範圍,比較偏 DDD 的答案是 application model 或是 use case 負責,一個 use case 就是一個完整的交易範圍,一個 use case 搭配一個 aggregate,透過 repository,整個 aggregate 物件儲存成功,或是全部失敗,不會有半個 aggregate 成功的情況,還可以搭配樂觀鎖 (參見閒談軟體設計:樂觀鎖),確保狀態的正確性。
不過「交易」這件事對我來說一直都是「技術細節」,個人不太喜歡在偏核心的 use case 中管理交易,反而是在 API controller 加上 @Transactional,確保每次 API 的呼叫,全部成功或全部失敗。也因此,這次的設計也是將交易管理做成一個 wrapper,可以搭配先前在閒談軟體設計:安全聲明設計的 secured 一起使用。但本文中討論的設計思維也可以用在別的地方,不是只能寫成 wrapper。
Service Locator
在確定由 API controller 管理交易範圍後,再來就是處理怎麼讓 repository 取得代表 transaction 的物件?一般來說,dependency injection 有幾種方式:
- constructor injection — 建立物件當下就注入需要的相依,但 repository 物件生命週期和 transaction 物件生命週期對不起來,難以用這個方式。
- method injection — 呼叫對應函式的當下提供需要的相依,上面的範例程式碼的
put(entity, session)便是這種方式,這方式破壞了 repository 原始的介面。 - setter injection — 透過 setter 將需要的相依注入,這種方式代表使用 repository 的物件要有 transaction 物件,不然也無法呼叫 setter 注入。
- interface injection — 實作特定 interface 讓框架知道需要注入特定的相依,會讓設計變複雜。
因為種種因素,上面幾種都不是這次要用的,我們需要一個可以先注入到 repository 中,等到 repository 需要時能提供 transaction 的物件,這時,service locator 就是蠻合適的方式。
從範例可以看到 SessionSource<Connection> 就是 service locator,在第 5 行的 constructor 就注入到 repository 中,而實際的 transaction 物件則是第 12 行才由 service locator 提供 (connection 參數),這讓 repository 維持原有的介面,同時在需要的時候可以取得 transaction 物件。
Thread Local
我們解決了 repository 怎麼取得 transaction 物件,但 SessionSource 長什麼樣子?如果有一個交易範圍需要用到兩個 repository 怎麼處理?不同的 API 請求怎麼隔離彼此的交易範圍?於是又再次請出在閒談軟體設計:日誌框架使用的 ThreadLocal。注意,這設計使用 ThreadLocal 有幾個前提要素:
- 每個請求都是由獨立的 thread 處理,
- 每個請求不會跨多個 thread (讓複雜度下降,不然每次切換 thread,必須要有對應的程式碼處理 transaction)
在閒談軟體設計:日誌框架中有提到,每個請求都會是一個新的 virtual thread 處理,處理完就銷毀,也沒有使用任何非同步式的框架,因此,這兩點在這次的設計中不是問題。
接著看 SessionSource 的介面,基本上只定義了兩個函式,一個處理無回傳值的,一個處理有回傳值的,當 repository 需要讀資料時用有回傳值的函式,當寫入或更新資料時用無回傳值的函式。
接著提供一個 TransactionManager 介面,主要就是提供開始、提交和回滾的操作。
然後便是實作 TransactionManager,內容其實蠻單純的,當要開啟一個 transaction 時,先檢查 thread local 有沒有已經有一個 transaction 了,如果有就略過,若沒有就建立一個並塞到 thread local 中。
呼叫提交時,檢查是否有 transaction,沒有則拋出例外,有就呼叫底層的提交,並將 transaction 從 thread local 移除;回滾也是類似的行為。
處理完,開啟、提交和回滾後,重點就是 runInSession 的實作了,如果沒有開啟 transaction,例如一個單純只讀取資料的 API,沒有其他特殊的原因,就可能不需要用 transational 包起來,此時就用獨立的 connection 來執行 (runInIsolatedTransactionalConnection)。
如果已經有一個開啟的 transaction,就會用既有的 transaction 來執行。因此,不管有幾個 repository,只要在同個 thread,使用相同的 session source,當已經有開啟的 transaction,都會用到同一個 transaction。
函式風格的聲明
底層的東西都完成差不多了,最後,提供一個 transactional 的函式,將 request handler 包起來,邏輯也很單純,就是在處理 request 前 (第 10 行),先呼叫 begin() (第 9 行),如果沒有錯誤,就呼叫 commit() (第 11 行),若出錯就呼叫 rollback() (第 14 行)。
看到這,可能還是覺得怪怪的,到底怎麼串起來的?整個流程會是這樣:
- client 呼叫 API
- Javalin 將請求導向 request handler
- 由於 request handler 已經被
transactional包起來,所以 wrapper 先呼叫begin - 接著呼叫真正的 request handler
- request handler 整理 request body 等參數後,呼叫 service (use case)
- service 呼叫 repository 儲存這次處理的物件
- repository 透過 session source 試圖取得 transaction,此時,會得到前面
begin建立的 transaction - 若一切正常,service 回傳資料
- request handler 將資料轉換成 response
- wrapper 呼叫
commit - response 回到 client
這大概是 OOP 為什麼需要一些前提知識,才能讀懂程式的原因,若沒有這些前提知識,透過中間層解耦的程式區塊,很難理解之間的關聯,但寫程式就是這樣,在各種不同的條件下取捨,本文的設計,是透過 service locator維持 repository collection-like 的介面,並用 thread local 將 transaction 的控制權交給 controller/use case 的一種做法。
小節
終於把閒談軟體設計:Web 框架的選擇中留下的坑補完了,雖然 Spring framework 變得越來越龐大,但確實提供了不少有用的東西,節省開發者不少時間,但 Spring framework 也並不是不得不的選擇,只是有些東西得開發者自己處理,例如本文提到的交易管理。
交易管理可能很複雜 (跨不同種的資料庫),老實說,能用現成被驗證過的技術就盡量用,這次因為選擇不使用 Spring framework,才需要自己手動寫一個,目前用起來沒太大問題,但之後若更複雜了,也許就要考慮成熟的第三方技術,但應該只需要改寫 transaction manager 就是了 (希望)。

















