领域仓储(Domain Repository)与事件存储(Event Store)是CQRS体系结构应用系统中C部分(Command部分)的重要组件。虽然都是存储机制,但两者有着本质的区别:领域仓储是属于领域层的,而事件仓储则是属于基础结构层的。领域模型产生事件,领域仓储负责保存、发布事件,并通过事件序列重塑领域模型。由于领域仓储的存在,使得“内存领域模型(In-memory Domain)”成为可能。
在上文中我已经对对象的状态做了一些介绍,通过这些介绍我们能够了解到,在应用系统中,是领域事件导致了对象状态的变化,于是,我们只需要把这些领域事件按顺序记录下来,我们就有能力将领域模型还原到任何一个时间点上。就以Tiny Library中的Reader聚合为例,当Reader刚刚被创建的时候,它的Name状态是空的,客户程序可以通过Reader实体的ChangeName方法来改变Name的状态。ChangeName方法会直接产生一个ReaderNameChangedEvent的领域事件,告知系统,现在发生了一件事情,这件事情将会改变Reader实体的状态。Reader实体获得了这个事件通知,就将Name状态设置为事件数据中的给定名称,同时,这个ReaderNameChangedEvent事件也被临时保存在了Reader实体中。
另一方面,当客户程序调用领域仓储来保存Reader时,仓储会将Reader中所有的领域事件读取出来,按照顺序逐个保存到事件存储中,与此同时,将这些事件发布到事件总线(Event Bus)上,以便同一系统的其它组件(比如Query Database)或者其它的系统能够接收到事件到达通知而做进一步的处理。
当客户程序需要通过领域仓储读取聚合时,领域仓储就会新建聚合,然后从事件存储中,以该聚合的聚合根的类型作为搜索条件,将领域事件按顺序读取出来并一个个地应用在这个新建的聚合上,聚合根实体一旦捕获到事件,就会按照事件的数据内容更新对应的状态,于是,聚合也就被恢复到了最后一次事件发生后的状态了。
这个过程很简单,通过上面的分析不难发现:
1: public interface IDomainRepository : IUnitOfWork, IDisposable
2: {
3: TAggregateRoot Get<TAggregateRoot>(long id)
4: where TAggregateRoot : class, ISourcedAggregateRoot, new();
5:
6: void Save<TAggregateRoot>(TAggregateRoot aggregateRoot)
7: where TAggregateRoot : class, ISourcedAggregateRoot, new();
8: }
1: // 查询事件存储
2: SELECT * FROM [Events] WHERE AggregateId=xxx ORDER BY Version
3:
4: // 向事件存储保存事件
5: INSERT INTO [Events] ([AggregateId], [Timestamp], [Version], [Data]) VALUES (...)
当然,在实际应用中,领域仓储与事件存储的实现并没有那么简单。原因可以通过如下几个疑问进行了解:
现在,我们再来看看Tiny Library CQRS项目中,事件存储的实现方式。实际上,Tiny Library CQRS采用的是Apworks应用开发框架所提供的默认的事件存储机制:基于SQL Server的单表事件存储。表结构如下:
首先,领域仓储从聚合获得未保存(即未提交)事件,然后,使用指定的序列化方式,将事件序列化为二进制流,并保存到Apworks.Events.Storage.DomainEventDataObject对象中,这个对象其实是一个DTO,它可以被序列化/反序列化,也可以被序列化为Data Contract而通过WCF在网络上自由传输。Apworks的基础结构层会通过DomainEventDataObject的属性定义,并结合一个给定的Storage Mapping Schema(也就是TinyLibrary.Services.DomainEventStorageMappings.xml文件),将DomainEventDataObject的数据保存到上面的数据表里。
在此简单介绍一下这个Storage Mapping Schema。由于我们使用的是关系型数据库,为了解耦“数据对象/属性”与“数据表/字段”的匹配,Apworks引入了Storage Mapping Schema,这个文件有点像NHibernate中的Mapping XML,但比NHibernate的Mapping XML简单很多:它不支持对数据对象关系与表关系的映射,它不是一个ORM。在Storage Mapping Schema中,仅仅简单地定义了数据对象/数据表,以及对象属性/字段的映射关系,这是由于,CQRS体系结构从实现上降低了关系型数据库的地位,定义数据表及其之间的关系已经不那么重要了。这里我又可以给出两种方案:如果你仍然希望在事件存储部分采用关系型数据库,并打算去维护复杂的数据表关系,那么,你可以不选用Storage Mapping Schema,而采用ORM(比如NHibernate),此时,DomainEventDataObject就是ORM上的“实体”;如果你不打算采用关系型数据库,而选择对象数据库(比如:Db4O),那么,你也不需要去维护任何的Mapping XML,对象数据库会帮你打理好一切,这将大大提高系统性能。以下是Storage Mapping Schema的XSD结构,以供参考。该XSD文件已被包含在Apworks应用开发框架的安装包里,用户可以在Apworks安装目录的scripts子目录中找到这个文件。
1: <?xml version="1.0" encoding="UTF-8"?>
2: <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
3: elementFormDefault="qualified"
4: attributeFormDefault="unqualified">
5: <xs:element name="StorageMappingSchema">
6: <xs:annotation>
7: <xs:documentation/>
8: </xs:annotation>
9: <xs:complexType>
10: <xs:sequence minOccurs="0">
11: <xs:element ref="DataTypes"/>
12: </xs:sequence>
13: </xs:complexType>
14: </xs:element>
15: <xs:element name="DataTypes">
16: <xs:complexType>
17: <xs:sequence minOccurs="0" maxOccurs="unbounded">
18: <xs:element ref="DataType"/>
19: </xs:sequence>
20: </xs:complexType>
21: </xs:element>
22: <xs:element name="DataType">
23: <xs:complexType>
24: <xs:sequence minOccurs="0">
25: <xs:element ref="Properties"/>
26: </xs:sequence>
27: <xs:attribute name="FullName" type="xs:string" use="required"/>
28: <xs:attribute name="MapTo" type="xs:string" use="required"/>
29: </xs:complexType>
30: </xs:element>
31: <xs:element name="Properties">
32: <xs:complexType>
33: <xs:sequence minOccurs="0" maxOccurs="unbounded">
34: <xs:element ref="Property"/>
35: </xs:sequence>
36: </xs:complexType>
37: </xs:element>
38: <xs:element name="Property">
39: <xs:complexType>
40: <xs:attribute name="Name" type="xs:string" use="required"/>
41: <xs:attribute name="MapTo" type="xs:string" use="required"/>
42: <xs:attribute name="Identity" type="xs:boolean" use="optional"/>
43: <xs:attribute name="AutoGenerate" type="xs:boolean" use="optional"/>
44: </xs:complexType>
45: </xs:element>
46: </xs:schema>
最后,在此给出Apworks应用开发框架中基于SQL Server的Event Store的类关系图,供大家参考。为了节省版面空间,此图中隐藏了类中的属性与方法定义,有兴趣的朋友可以到Apworks的站点http://apworks.codeplex.com上查看具体的代码实现。
在下一篇文章中,我将向大家介绍Tiny Library CQRS项目中,事件总线(Event Bus)与消息派送器(Message Dispatcher)的设计与实现,敬请期待!