使用Spring Data JPA實現DDD聚合的動態投影

banq發表於2024-04-16

投影是從儲存庫載入的DDD聚合 的子集,用於只讀目的。

返回投影的方法通常在儲存庫級別上定義,使儲存庫介面瞭解應用程式中使用的所有可能型別的投影。

package com.app.account.domain;

public interface AccountRepository extends Repository<Account, String> {
    AccountBasic findAccountBasicById(String id);
    AccountComplete findAccountCompleteById(String id);
}

public record AccountBasic(String id, 
                           String iban,
                           String bic) {}

public record AccountComplete(String id,
                       String iban,
                       String bic,
                       State state,
                       Type type,
                       LocalDateTime createdAt) {}


這種方法有一些缺點:

  • 如果有一個投影幾乎適合我們的用例,那麼很容易修改它並新增所需的欄位 - 同時有點違背了投影的主要目的 - 只獲取需要的內容。
  • 命名投影成為一個挑戰——如何命名它們?AccountBasic?AccountLight?AccountData?所有這些名字都毫無意義。
  • 儲存庫變得很難使用,因為它包含一長串方法,並且要找出應該使用哪個方法,您需要訪問每個投影。
  • 由於投影在用例之間共享,因此它們成為耦合點,使未來的重構更加困難。
  • 投影必須與儲存庫具有相同的可見性級別,例如,如果儲存庫是公共的,則投影也必須是公共的。

我們可以使用動態投影,而不是讓儲存庫瞭解所有用例使用的所有型別投影。

動態投影
動態投影是 Spring Data JPA 鮮為人知和較少使用的功能之一。
動態投影允許您在儲存庫級別定義採用任何型別投影的通用方法,並且不知道投影的內容是什麼樣?

package com.app.account.domain;

public interface AccountRepository extends Repository<Account, String> {
    <T> T findById(String id, Class<T> projection);
}

現在,在任何包中我們都可以定義一個投影介面或一條記錄,只要它與實體的結構匹配Account,就可以與這個動態儲存庫方法一起使用:

package com.app.account;

record AccountBasic(String id, 
                    String iban, 
                    String bic) {}

<font>// ...<i>
AccountBasic account = accountRepository.findById(id, AccountBasic.class);

在本示例中,AccountBasic 是 com.app.account 中的一個包私有類。只有該軟體包中的類才能訪問它,因此不存在在應用程式其他部分濫用該投影的風險。

本地記錄投影
透過使用本地記錄,我們可以將隱藏投影的可視性提升到一個新的水平。

在方法內部定義的記錄被稱為本地記錄,它是使用案例的完美解決方案,在使用案例中,投影僅在該方法內部使用,不會返回:

class BankTransferService {
    private final AccountRepository accountRepository;
    
    <font>// ...<i>
    
    void transferMoney(String sourceAccountId, String targetAccountId) {
        record AccountBasic(String id,
                            String iban,
                            String bic) {}
        
        var source = accountRepository.findById(sourceAccountId, AccountBasic.class);
        var target = accountRepository.findById(targetAccountId, AccountBasic.class);
       
// ...<i>
    }
}


動態投影的缺點
這種方法有一些缺點值得考慮:

  • 儲存庫不再是檢視從應用程式程式碼執行的所有可能查詢的單一位置
  • 經過測試的儲存庫並不意味著測試了與資料庫的所有互動 - 任何使用動態投影的地方都必須有相應的使用真實資料庫的整合測試
  • 返回動態投影的儲存庫方法部分where僅從方法名稱解析 - 無法編寫自定義JPQL或SQL查詢

相關文章