什么是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)。
欢迎参与讨论,写的有问题的地方亦欢迎指正,嘿嘿。
[01] CQRS实践系列
Byteart Retail项目简介
从2007年至今,我一直关注着与领域驱动设计相关的软件开发过程与技术,在这几年中,我坚持不懈地学习、实践,在总结自己实践经验的基础上,设计并开发了一套基于.NET的面向领域驱动的企业应用程序开发框架,沿用我以前开发的一个松耦合架构实验原型,为之取名为Apworks。为了向社区展示Apworks在企业级应用开发上给开发人员带来的便捷,我也针对Apworks框架开发了一套面向CQRS架构的案例程序:Tiny Library CQRS。随着Apworks的不断发展,Tiny Library CQRS也先后更新了两个版本,毋庸置疑,第二版更为成熟,更贴近于实际项目应用。
然而,社区对Tiny Library CQRS的反馈却不是那么积极,分析原因,我觉得有三个方面:首先,基于事件溯源(Event Sourcing)机制的CQRS架构本身就非常复杂,套用世界级软件架构大师Udi Dahan对CQRS架构的总结,就是:“简单,但不容易(Simple, but not easy)”,要让一个对企业级软件架构设计不太熟悉的开发人员掌握好CQRS相关的知识,是一件困难的事情,即使有现成的案例,也会让人感觉无从下手;其次,目前大多数应用程序还远没达到需要使用CQRS架构的规模,在项目中应用CQRS,只能把简单问题复杂化;再次,由于个人时间能力有限,Tiny Library CQRS案例本身也没有提供太多的文档与说明,加上该案例直接使用了Apworks框架,所以很多后台运行机制就变得不那么明朗,这对希望研究CQRS架构的开发人员造成了一定的困难。因此,Tiny Library CQRS感觉就与真实项目的实践脱节,自然关注的人就不多了。所以我打算暂时搁置Tiny Library CQRS的更新,让其也成为一个CQRS架构设计的参考原型,供有兴趣的朋友参观学习。
于是,从今年4月开始,我就着手开发并发展了另一个面向领域驱动的.NET企业级应用架构设计案例:Byteart Retail。与Tiny Library CQRS不同的是,Byteart Retail采用了面向领域驱动的经典分层架构,并且为了展示微软.NET技术在企业级应用开发中的应用,它所使用的第三方组件也几乎都是微软提供的:Entity Framework、ASP.NET MVC、Unity IoC、Unity AOP、Enterprise Library Caching等(用于记录日志的log4net除外,但log4net本身也是众所周知的框架),所以,开发人员只需要打开Byteart Retail的源程序,就能够很清楚地看到系统的各个组件是如何组织在一起并协同工作的。经典分层架构的采用,也为实际项目带来了参考和指导的价值。
Byteart Retail所使用的技术
Byteart Retail项目使用或涵盖了以下Microsoft技术:
- Microsoft Entity Framework 5 Code First(包括Repository模式的实现、枚举类型的支持以及分页功能的实现)
- ASP.NET MVC 4
- WCF
- Microsoft Patterns & Practices Unity Application Block
- Microsoft Patterns & Practices Unity Policy Injection Extension
- Microsoft Patterns & Practices Caching Application Block
- Microsoft Appfabric Caching
- 使用AutoMapper实现DTO与领域对象映射
- T4自动化代码生成
- 基于Unity的AOP拦截
- 使用log4net记录拦截的Exception详细信息
Byteart Retail所演示的模式和设计思想
Byteart Retail项目演示或涵盖了以下模式和设计思想:
- 实体、值对象、领域服务
- 规约、仓储、仓储上下文
- 领域事件、事件聚合器、事件总线
- 事务协调器
- 服务定位器模式、工作单元模式、分离接口模式、数据传输对象模式、层超类型模式、传输对象组装器模式
运行Byteart Retail案例
先决条件
从V3开始,本案例使用Visual Studio 2012开发,因此,要编译本案例的源代码程序,则需要首先安装Visual Studio 2012。由于数据库采用了SQL Server Express LocalDB,因此,这部分组件也需要正确安装(如果是选择完整安装Visual Studio 2012,则可以忽略LocalDB的安装)。 另外,Byteart Retail提供了两种事件总线(Event Bus)的实现:一种是面向事件聚合器(Event Aggregator)的,它将把所获得的事件通过聚合器派发到一个或多个事件处理器上;另一种是面向微软MSMQ的,它将把所获得的事件直接派发到MSMQ队列中,如果采用这种事件总线,则需要在机器上安装和配置MSMQ组件,并确保新建的队列是事务型队列。 此外,无需安装其它组件。
编译运行
克隆源代码资源库,或者直接下载zip压缩包,然后在Microsoft Visual Studio 2012中打开ByteartRetail.sln文件,再将ByteartRetail.Web项目设置为启动项目后,直接按F5(或者Debug –> Start Debugging菜单项)运行本案例即可。注意:
- 如果不打算以Debug的方式启动本案例,那就需要首先展开ByteartRetail.Services项目,任选其中一个.svc的服务文件(比如UserService.svc)然后点击右键选择View In Browser菜单项,以便启动服务端的ASP.NET Development Server;最后再直接启动ByteartRetail.Web项目
- 由于Byteart Retail V3的数据库采用的是SQL Server 2012 Express LocalDB(默认实例),在程序连接LocalDB数据库时,LocalDB需要创建/初始化数据库实例,因此在首次启动时有可能会出现数据库连接超时的异常,如果碰到这类问题,则请稍等片刻然后再重试
- 如果以上述第一点的方式运行ByteartRetail.Web项目并出现与WCF绑定相关的错误时,这表示WCF服务并没有完全启动,请重新启动ByteartRetail.Services项目,然后再启动ByteartRetail.Web项目
登录账户
启动成功后,就可以单击页面右上角的“登录”链接进行账户登录。默认的登录账户有(用户名/密码):
- admin/admin:以管理员角色登录,可以对站点进行管理
- sales/sales:以销售人员角色登录,可以查看系统中订单信息并进行发货等操作
- buyer/buyer:以采购人员角色登录,可以管理商品分类和商品信息
- daxnet/daxnet:普通用户角色,不能对系统进行任何管理操作
解决方案结构
ByteartRetail.sln包含以下项目:
- ByteartRetail.Design:包含一些设计相关的图画文件,仅供参考,没有实际意义
- ByteartRetail.Application:应用层
- ByteartRetail.DataObjects:数据传输对象及其类型扩展
- ByteartRetail.Domain:领域层
- ByteartRetail.Domain.Repositories:仓储的具体实现(目前是基于Entity Framework 5.0的实现)
- ByteartRetail.Events:事件相关的事件处理器、事件总线和事件聚合器的定义
- ByteartRetail.Events.Handlers:具体的事件处理器定义
- ByteartRetail.Infrastructure:基础结构层
- ByteartRetail.Infrastructure.Caching:位于基础结构层的缓存实现
- ByteartRetail.ServiceContracts:基于WCF的服务契约
- ByteartRetail.Services:WCF服务
- ByteartRetail.Web:基于ASP.NET MVC的站点程序(表示层)
总结
热烈欢迎爱好Microsoft.NET技术以及领域驱动设计的读者朋友对本案例进行深入讨论。同时也欢迎访问我的.NET/DDD架构经验分享交流网站:http://apworks.org