DDD 與 Spring Data MongoDB

IoC 註解主要有兩大類:

(1) 聲明 Bean 的註解:告訴別人自己是 bean;個組件對象,把自己聲明成 bean。

(2) 注入 Bean 的註解:在一個 bean 中注入另外一個 bean,有依賴關係的 Bean 相互注入的註解。

聲明式(1)告訴別人自己是 bean

聲明 Bean 的註解如下。實際上,後 3 個註解的功能和第 1 個一致。但如果我們能夠明確 bean 的層次,最好用後 3 種,這樣代碼可讀性比較高。

    該註解用於標註一個控制器組件類(Spring MVC 的 Controller),其功能與 @Component() 相同。

註解 2-4,是與 MVC 中的架構來對應的。

@Controller, @Service, @Component, @Repository

其中 @Component 是一種通用名稱,泛指任意可以通過 Spring 來管理的組件,@Controller, @Service, @Repository 則是一種特定的組件,通常用來表示某種特定場合下的組件,@Repository 用來表示倉庫(數據層,DAO),並且 Spring 框架會根據這種應用場景做些定製,比如 @Repository 同時具備了自動化的異常轉換。類似的, @Service 則用來表示服務層相關的類, @Controller 則用來表示展示層(presentation)的類。

那 Service 是什麼呢?

Service 表示了在軟件分層設計中的 Service 層,用來連結數據層(DAO)和展示層(Presentation)。

爲什麼要在 DAO 層上加一層 Service 呢?

在某些簡單的應用中,DAO 層的功能和 Service 的功能很接近,甚至初學者會覺得 Service 層做的事情和 DAO 層都一樣,那爲啥還要將 Service 層單獨拿出來做一遍呢?而且,很多場景下,Service 層和 DAO 層同時存在,往往會增加代碼複雜度,編碼工作量,寫的不好甚至會造成混淆。

通常來說,DAO 層應盡力保持簡單,其功能僅僅是提供了數據庫的連接,以及最簡單的增刪改查(Crud),有時還需要做些抽象,以此來連接使用不同技術的數據庫。除此之外,任何業務相關的操作都應該放到 Service 層,即 Service 層用來編寫業務邏輯,即操作從 DAO 層讀取的數據,或者將處理好的數據給 DAO 層,當使用 Domain Driven Design 時, 這兩個類通常會放到同一個 Domain(包)中,即便在簡單的應用中,他們的代碼可能極其類似,但是仍應該分別對待。而不是跳過 service 層(service)直接去使用 DAO 層(repository)來放業務邏輯數據。

這樣帶來的好處帶來更好的模塊化結構,有便於後期的擴展和維護,比如更換數據庫實現時,我們僅僅需要處理 DAO 層的內容就好了。並且,當業務邏輯比較複雜的時候,比如有很多報告要出的時候,Service 層就提供了一個很好的空間來實現這些代碼。其次,在 web 應用開發中,使用 Service 層可以將 web 類的活動限制在 controller 中,這樣可以獨立的測試 service 層

另外,還有一種情況,就是當應用極其複雜,需要同時使用多種數據庫時,將從 DAO 中獲取數據的動作放到一起可以減少數據庫的操作,並且可以保證數據的一致性。同時 Service 可以嵌套,因此如果需要使用不同的數據庫時,可以在 service 中指定。

在 Service 中也可以放一些通知類的操作,比如發送郵件等,這樣也可以保持 controller 的整潔。

還有一個潛在的好處是安全性,當使用 service 層包裹 DAO 層後,數據庫的鏈接是被 service 層保護起來的,這樣如果客戶端被某種情況攻陷,其只能使用 service 層提供的有限數據,而無法直接攻擊數據庫

另外,在 Spring 框架中,security 也是在 Service 層實現的。根據上面的邏輯,我們在實際開發中,應該不去實現自己的 DAO 層,而是使用 Spring Data JPA,因爲 Spring Data JPA 已經實現了 DAO 層。

這種寫法常見的問題有啥?

最常見的寫法(或者是錯誤的寫法)有以下幾種

1、面向領域的模型對象僅僅用來存儲應用中的數據,換句話說,是不太符合 domain model 設計的

2、處理模型數據的業務邏輯分散在 service 層

3、每個 entity 都有對應的 service 類

這樣寫的原因很大程度來源於上面的分層理論,我們確實將應用分成了展示層(web layer),服務層(service layer),數據層(repository/dao),但是實際後果卻是一個極其龐大的 service 層,這種寫法可以算是一個面向過程開發的代碼(procedural code), 而不是面向對象開發。好處是簡單,當業務不復雜時,確實沒有必要使用一個龐大的面向對象開發框架(domain driven design)。

一個責任並不明確的 service 層主要有以下問題

1、業務邏輯分散在 service 層中,當我們需要確認或者檢查某個業務邏輯時,可能要在多個 service 類中尋找,也許並不那麼容易,另外如果同樣的業務邏輯在多個 service 類中用到時,那麼可能會存在大量的重複代碼,這種重複代碼對於維護人員來說就是惡魔。

2、在 service 層中,每個 entity 都有對應的 service 類時,service 層會有過多的依賴,甚至是循環依賴關係,而不是由鬆散耦合的 service 類構成 service 層,理想中的 service 層應該是由具有單一責任的 service 類構成,並且這些 service 類具有松耦合關係,如果不是這樣的 service 層,將難以理解,維護和重用。

主要的解決方法是

1、將與 entity 相關的業務邏輯統一放到領域模型對象相關的類中,即所謂的 domain service 中。這樣做的好處時,傳統概念中的 service 層僅僅處理應用相關的業務邏輯,即作爲 Application Service。然後 domain service 中處理 domain 內的業務邏輯。業務邏輯將按照 domain 和 application 的方式分開,容易定位和維護。傳統意義上的 applicationservice 層將變得整潔。

2、在 domain service 中我們將按照 entity 來編寫對應的 service,這些都是特定的 service,很小,僅僅面對很專一的功能。舉例來說,如果應用中的某個 service 提供 person 類的 crud, 同時還提供用戶帳號的操作,那麼我們應該將 person 的 crud 單獨放到一個 service 中,然後將用戶帳號相關的操作放到另一個 service 中。

所有這些分層方式都是爲了解決應用從小項目成長爲大項目時可能遇到的隱患,代價是在項目還小時,增加了項目的複雜度,往往一句代碼就能搞定的事情,卻要拆到三個類中去。但是太多的實際例子表明,如果沒有好的架構,當小項目膨脹到一定程度時,往往是無法維護的,只能全部推倒重寫。

在 Domain Driven Design 中如何區分各種 Service?

在 DDD 中,service 有三種類型

Domain Service

Domain Service: 用於放置領域對象相關的業務邏輯,這些業務邏輯通常並不適合放到 entity 中,也不是常見到的 CRUD(這些應該放到 Repository), 將 Domain Service 和 Domain Objects 放到一起是合理的,它們都是關注於 domain 相關的業務邏輯。在 Domain Service 中可以使用注入 repository 的方式來使用 entity 對應的 repository。

舉一個例子:

一個圖書館有三個 entity:Book, Client,Inventory, 當把一本書借給一個客戶時,就對應了一個 Domain Service。在一個例子,在 Eric Evans 的《Domain Driven Design》書中,轉賬服務(FundsTransferService)也是一種 domain service,它涉及到帳號 BankAccount,但是並不適合放到 BankAccount 中。

Application Service

Application Service: 用於爲應用外的 client 或 consumer 提供應用級別的服務,比如一個外部客戶端(程序)需要使用某個 entity 的 CRUD 時,這些服務程序放到 Application Service。Application Service 通常會使用 Domain Service 和 repository 來處理外部的請求。常見的場景是,從 repository 中拿到一些 domain objects, 然後執行某些操作,在將其放回 repository(或者不放), Application Service 對應着大部分用戶使用場景,在寫一個應用時,可以先從 Application service 寫起,這樣可以很好界定應用的功能和範圍。repository 雖然可以在某些場景下注入到 domain service 中,但是更常見的是注入到 applicatinoservice 中。

Infrastructure service

還有一種 Infrastructure service:用於抽象一些技術問題,比如消息隊列,郵件服務

具體例子 spring-petclinic

Domain Driven Design 目錄結構層級


├── Application.java

├── assembler                       DTO 組裝器:將領域對象組裝成 DTO

├── config                          配置文件目錄

├── constant                        常量、枚舉目錄

├── domain                          領域層

│   ├── event                       領域事件:MQ event

│   ├── model                       領域對象

│   ├── repository                  領域倉庫

│   ├── service                     業務領域服務

│   └── translator                  翻譯器:將持久化層 Entity 或 DTO 翻譯爲領域對象。

├── infrastructure                  基礎設施目錄

│   ├── exception                   異常

│   ├── interceptor                 權限攔截

│   ├── repository                  領域層倉庫具體實現

│   │   ├── dao                     mysql、oracle、mongodb

│   │   │   └── impl

│   │   ├── mock                    啞實現

│   │   │   └── impl

│   │   └── redis                   redis 緩存

│   │       └── impl

│   ├── runner                      程序啓動初始化 runner

│   ├── task                        定時任務

│   ├── transport                   第三方服務交互

│   ├── util                        工具類

│   └── validation                  校驗層

├── service                         應用服務層

└── ui                              對外開放的用戶層,User Interface

    ├── controller                  控制器

    └── dto                         用於外部數據交互的 DTO

        ├── request

        └── response

框架說明

User Interface 用戶層

用戶層,對外以各種協議提供服務,包含:

DTO

包括 request 和 response 兩部分,通過它定義入參和出參的契約,在 DTO 層可以使用基礎設施層的 validation 組件完成入參格式校驗

Controller

Controller 使用基礎設施層公共組件完成通用的工作:

通過註解 RequestMapping 添加 servlet 路由

通過 interceptor 完成登錄權限 / 角色校驗

簡單的數據轉換

Application Layer 應用層

Service

應用服務層,組合 domain 層的領域對象和基礎設施層的公共組件,根據業務需要包裝出多變的服務,以適應多變的業務服務需求。

應用服務層主要訪問 domain 領域對象,完成服務邏輯的包裝。

應用服務層也會訪問基礎設施層的公共組件,如 redis,完成領域消息的生產和獲取等。

Assembler

組裝器,負責將多個 domain 領域對象組裝爲需要的 DTO 對象,比如查詢帖子列表,需要從 Post(帖子)領域對象中獲取帖子的詳情,還需要從 User(用戶)領域對象中獲取用戶的基本信息。

組裝器中不應當有業務邏輯在裏面,主要負責格式轉換、字段映射等職責。

Domain Layer 領域層

業務領域層,是我們最應當關心的一層,也是最多變的一層,需要保證這一層是高內聚的。確保所有的業務邏輯都留在這一層,而不會遺漏到其他層。按照 DDD(domain driven design)理論,主要有如下概念構成:

Domain Entity

領域實體對象。有唯一標識(主鍵),可變的業務實體對象,有自己的生命週期。比如門店就是一個業務實體,它需要有一個唯一性業務標識表徵(門店 ID),同時它的狀態(開店 / 停業)和內容(名稱 / 地址)可以不斷髮生變化。

Domain Value Object

領域值對象。可以沒有唯一性業務標識,且一旦定義,就不可變的,它通常是短暫的。這和 Java 中的值對象(基本類型和 String 類型)類似。比如門店領域中,門店的位置信息可以理解爲是一個值對象,不需要爲門店的位置信息定義唯一標識,直接使用門店 ID 就可以找到每個門店的位置信息。同時,它具有省 / 市 / 區和詳細地址幾個屬性,一旦任一個屬性發生變化,則需要重建這個位置信息對象並賦值給門店實體的引用。

Domain Factory

領域對象工廠。用於複雜領域對象的創建 / 重建。重建是指通過 respostory 獲取到對象後,重建或組裝多個對象爲領域對象。

Domain Service

領域服務。區別於應用服務,它屬於業務領域層。

可以認爲,如果某種方法無法歸類給任何單一實體 / 值對象,則就爲這些方法建立相應的領域服務即可。比如:門店收銀服務,需要操作門店 / 會員兩個實體。

傳統意義上的 util static 方法中,只要涉及到業務邏輯的部分,都可以考慮歸入 domain service。

Domain Event

領域事件。領域中產生的一些消息事件,通過事件通知 / 訂閱的方式異步操作,可以在性能和解耦層面得到好處。

Repository

領域倉庫。我們將倉庫的接口定義歸類在 Domain 層,因爲它和 Domain entity 聯繫緊密。User Interface 和基礎實施的持久化層交互,都需要實現領域倉庫接口對應的增刪改查操作。

倉庫的實際實現根據不同的存儲介質而不同,可以是 redis、mysql 等。

鑑於存儲介質可以同時存在有多套:redis、mysql,memory cache, 且各個存儲介質的字段屬性名不一致,因此需要使用 translator 來做翻譯,將持久化層的對象翻譯爲統一的領域對象。

Translator

翻譯器。將持久化層的對象翻譯爲統一的領域對象。

翻譯器中不應當有業務邏輯在裏面,主要負責格式轉換、字段映射等職責。

Infrastructure Layer 基礎設施層

基礎設施層提供公共功能組件,供 controller、service、domain 層調用。

Repository impl

對 domain 層 repository 接口的實現,對應每種存儲介質有其特定實現,如 mysql、oracle 的 dao 等等。repository impl 會調用 mybatis、jooq、redis client 完成實際的存儲層操作。

Interceptor

權限校驗器,判定客戶端是否有訪問該資源的權限。提供給 User Interface 層的 Controller 調用。

Exception

異常分類及定義,同時提供公共的異常處理邏輯,具體由 ExceptionHandler 實現。

Transport 第三方服務交互層

transport 完成和第三方服務的交互,可以有多種協議形式的實現,如 http+json、二進制文件流,用於對第三方服務的請求和響應進行適配,提供一個防腐層的作用。

注意要點

各個 package 的詳細解釋參考上節框架說明,着重注意如下幾點:

domain.repository 包裏面只有倉庫的接口定義,實際的實現交給了 infrastructure 中的 repository module,該做法被稱作依賴倒置,好處在於確保 domain 層語義完整,同時對確保業務領域的一致性也有幫助,再者可以在 domain 實現內存形式的 repository 啞實現,從而讓 domain 可以真正脫離掉 infrastructure

infrastructure.repository 作爲倉庫層,會將實體的增刪改查操作委託給具體的存儲層服務,如 mysql 對應的 dao 實現,還有 redis 的實現

少寫應用層 Service,多把業務邏輯封裝到 Domain

Controller 要瘦,僅處理消息或數據轉發,Model 僅處理領域邏輯。而 Service 通常負責領域間的交互及某些預處理(比如數據驗證、權限驗證等等)

NoSQL 數據庫

NoSQL 數據庫是持久化數據的另一種方式,但是與關係數據庫的表格關係不同。這些新興的 NoSQL 數據庫已經有一個分類系統。您可以根據其數據模型找到它。

• Column (Cassandra, HBase, etc.)

• Document (CouchDB, MongoDB, etc.)

• Key-Value (Redis, Riak, etc.)

• Graph (Neo4J, Virtuoso, etc.)

• Multimodel (OrientDB, ArangoDB, etc.)

如您所見,您有很多選擇。我認爲最重要的功能是找到一個可擴展的數據庫,並可以輕鬆處理數百萬條記錄。

Spring Data MongoDB

Spring Data MongoDB 項目爲您提供了與 MongoDB 文檔數據庫的必要交互。重要功能之一是您仍然可以使用使用 @Document 批註的域模型類並聲明使用以下內容的接口 CrudRepository <T,ID>。這將創建 MongoDB 用於持久性的必要集合。

以下是該項目的一些功能。

•Spring Data MongoDB 提供了 MongoTemplate 幫助器類(非常

與 JdbcTemplate 相似)處理所有樣板交互與 MongoDB 文檔數據庫一起使用。

• 持久性和映射生命週期事件。

•MongoTemplate 幫助器類。它還使用 MongoReader / MongoWriter 抽象提供低級映射。

• 基於 Java 的查詢,條件和更新 DSL。

• 地理空間和 MapReduce 集成以及 GridFS 支持。

• 對 JPA 實體的跨存儲持久性支持。這表示您可以使用標有 @Entity 和其他標記的類批註,並使用它們來保留 / 檢索數據 MongoDB 文檔數據庫。

帶有 Spring Boot 的 Spring Data MongoDB

要將 MongoDB 與 Spring Boot 結合使用,您需要添加 spring-boot-starter-datamongodb 依賴項並有權訪問 MongoDB 服務器實例。

Spring Boot 使用自動配置功能來設置一切以便與 MongoDB 服務器實例進行通信。默認情況下,Spring Boot 嘗試連接到本地主機並使用端口 27017(MongoDB 標準端口)。如果您有 MongoDB 遠程服務器,則可以通過覆蓋默認值連接到該服務器。您需要使用 application.properties 文件中的 spring.mongodb。* 屬性(簡便方法),或者可以使用 XML 或 JavaConfig 類中的 bean 聲明。

Spring Boot 還會自動配置 MongoTemplate 類(該類與 JdbcTemplate 非常相似),因此可以與 MongoDB 服務器進行任何交互。另一個很棒的功能是您可以使用存儲庫,這意味着您可以重用用於 JPA 的相同接口。

嵌入式 MongoDB

還有另一種使用 MongoDB 的方法,至少作爲開發環境。您可以使用 MongoDB Embedded。通常,您可以在測試環境中使用它,但是可以在帶有運行時範圍的開發模式下輕鬆運行它。

<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<scope>runtime</scope>
</dependency>

清單 5-8 顯示了 MongoClient bean 的配置。當應用程序啓動時,MongoDB Embedded 將使用隨機端口,這就是爲什麼還必須使用 Environment Bean 的原因。如果您採用這種方法來使用 MongoDB 服務器,則無需設置任何其他屬性。

清單 5-8 顯示了 MongoClient bean 的配置。當應用程序啓動時,MongoDB Embedded 將使用隨機端口,這就是爲什麼還必須使用 Environment Bean 的原因。

如果您採用這種方法來使用 MongoDB 服務器,則無需設置任何其他屬性。

清單 5-9 顯示了修改後的 ToDo 類。該類使用 @Document 批註將其標記爲持久性;它還使用 @Id 聲明唯一鍵。

如果您有遠程 MongoDB 服務器,則可以覆蓋指向本地主機的默認值。您可以轉到 application.properties 文件並添加以下屬性。

[root@repo resources]# pwd

/david/pro-spring-boot-2-master/pro-spring-boot-2nd/ch05/todo-mongo/src/main/resources

[root@repo resources]# cat application.properties

MongoDB

spring.data.mongodb.host=localhost

spring.data.mongodb.port=41906

spring.data.mongodb.username=springboot

spring.data.mongodb.password=pivotal

查看應用啓動日誌:

db.textIndexTest.find({$text:{$search:"王"}})

{"_id" : ObjectId("6058791f29f9c258cf4d6921"), "author" : "張 王", "title" : "你好", "article" : "測試數據" }
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3jxQfLNwGwa8JkdhyGyQow