CQRS實踐(1): 什么是CQRS
什么是CQRS?
這個問題網上可以找到很多資料,未接觸過的童鞋請先查看Udi Dahan, Grey Young, Rinat Abdullin,園子里dax.net,以及Jdon社區上的相關文章。
例如下面幾篇文章:
1. http://www.rzrgm.cn/daxnet/archive/2011/01/06/1929099.html
1. http://www.udidahan.com/2009/12/09/clarified-cqrs/
2. http://www.jdon.com/jivejdon/thread/37891
這里只通過Udi Dahan的《Clarified CQRS》文章中的一張圖片簡要介紹一下:

UI上有兩種類型的操作:命令和查詢,例如顯示銷量最好的5個產品就屬于查詢,而提交一個訂單、修改密碼等則屬于命令。因為大部分系統都是讀多寫少,而且業務邏輯基本都出現在寫入的一端,所以查詢和命令的分離可以讓我們獨立的去優化查詢。
查詢 (Query)
上圖中,可以看到Query不是通過DB來查詢,而是通過一個專門用于查詢的Read DB(上圖中的Cache,它不一定是數據庫,但為方便起見,下面統稱Read DB),Read DB中的表(方便起見,暫且認為這個Read DB是一個RDBMS)是專門針對UI優化過的,例如里面可能會有LatestProductListModel(ProductId, ProductName, Price, BrandName, AddedTime)、BestSoldProductListModel(ProductId, ProductName, TotalSold)這樣的表,分別表示最新的產品列表,銷量最好的產品列表(它們其實就相當于是View Model)。LatestProductListModel中有一個BrandName的字段,注意,不是BrandId,因此,對于界面中的查詢,幾乎全都可以通過SELECT * FROM [TABLE]這樣的SQL語句來實現,可能有少數Where,但基本沒有Join,這對于界面的加載速度絕對是有利無弊的(其實也是在用空間換時間)。
命令 (Command)
業務邏輯大部分都發生在寫入的時候,例如用戶購買商品提交訂單時,我們要驗證庫存,用戶信息訂單數據是否有效等。如果從傳統DDD的角度看,Command類似于Application Service,用戶的命令(如提交訂單)會以Command的形式得到執行,而Command中也不會帶有業務邏輯,Command中做的事情基本上是:通過Repository得到相關的領域對象,調用某些領域服務(Domain Service)執行一些操作(業務邏輯都將保留在領域模型中),然后執行Commit或SaveChanges之類的方法提交改動,之后,相關的數據就會寫入到Write DB中(圖的DB,下文統稱Write DB)。需要注意的是,UI上的查詢都是查Read DB,而不是Write DB。
領域模型 (Domain Model)
這和Evans的DDD中說的領域模型沒有太多區別,是“the heart of software”。
領域事件 (Domain Event)
領域事件占據的地位非常重要,不僅限于CQRS。相信會有一部分人曾和我一樣碰到過這樣的問題:
Account實體(表示帳戶)有個Balance屬性(表示帳戶余額),我們一般不會公開這個屬性的setter,而是通過寫一些IncreaseBalance(decimal amount)之類的方法來實現帳戶余額的變動。
這時問題就來了,我們想在帳戶變動時添加一條AccountLog記錄,但Log記錄成千上萬,我們不能直接通過ORM的一對多映射把AccountLog集合實現成Account的一個集合屬性,那我們就需要在IncreaseBalance()中得到AccountLogRepository,這樣才有辦法插入AccountLog(從DDD的角度,AccountLog不是聚合根,所以不能有AccountLogRepository,但在性能影響嚴重的時候,也只好做些取舍了)。
不管用了依賴注入還是什么的,總之,Account已經依賴上Repository了,這就讓領域對象變得很不純凈,并且,假如我們以后不僅要記錄log,還要短信通知用戶呢?那要修改源代碼嗎?這也很不OCP。
而領域事件正好可以解決這種問題:只要在IncreaseBalance()方法的末尾,觸發一個領域事件,然后我們獨立寫一個EventHandler的類去實現log的添加(框架可以保證EventHandler可以和領域事件綁定到一起)。
回到CQRS,因為Command將數據寫到了Write DB中,而UI查詢的是Read DB,那我們就需要用某種方式實現這兩個數據庫的同步,解決辦法已經很明顯了,寫一堆的EventHandler類去監聽領域事件。例如我們有一個更改產品價格的命令ChangePriceCommand,它執行后,一個叫做PriceChangedEvent會被觸發,那我們只要寫一個PirceChangedEventHandler的類,在這里面將Read DB中相關的價格信息更改到最新值即可實現同步(這里會涉及到Read DB中表結構改變的問題,后面再說)。
結語
CQRS有意思的地方還不只這些,還有常和CQRS一起討論的Event Sourcing(事件溯源,下面簡稱ES)等。
總得來說,CQRS看起來很迷人,但在自己的實踐過程中,碰到了各種各樣的問題,尤其ES,這幾乎顛覆了平常的開發思維。例如,使用了ES后,領域模型只能通過Id來查詢,如果你想查詢姓名為“水哥”的用戶,是做不到的,因為不會存在一個叫做User的表。相信大部分剛接觸ES的朋友都會對此感到不適應。這需要思維上的改變。
后續的幾篇文章里,我會繼續分享自己在CQRS實踐過程中碰到的各種感覺比較典型的問題以及我目前能找到的最好方案(更希望到時有童鞋有更好的方案分享)。然后通過實現一個迷你型的CQRS框架以及基于其開發的一個BookStore示例項目來展示CQRS所帶來的好處。
這個迷你框架和示例項目中將會對常討論的CQRS進行簡化,剔除掉個人感覺和平常開發跨度比較大的東西,例如ES,異步Command等,同時還會針對平常習慣的開發方案做一些取舍,例如UI中可以根據需要混合查詢Read DB和Write DB(前提是在Write DB的查詢也很簡單的情況下,比如同樣只需要一個SELECT)。
歡迎參與討論,寫的有問題的地方亦歡迎指正,嘿嘿。
浙公網安備 33010602011771號