什麼是CQRS?
這個問題網上可以找到很多資料,未接觸過的童鞋請先查看Udi Dahan, Grey Young, Rinat Abdullin,園子裡dax.net,以及Jdon社區上的相關文章。
例如下面幾篇文章:
1. http://www.cnblogs.com/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)。
歡迎參與討論,寫的有問題的地方亦歡迎指正,嘿嘿。