求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
CQRS体系结构模式实践案例:Tiny Library
 

作者:陈晴阳,发布于2013-1-28,来源:博客园

 

目录:

CQRS体系结构模式实践案例:Tiny Library:简介

CQRS体系结构模式实践案例:Tiny Library:系统架构

CQRS体系结构模式实践案例:Tiny Library:对象的行为和状态

CQRS体系结构模式实践案例:Tiny Library:领域仓储与事件存储

CQRS体系结构模式实践案例:Tiny Library:简介

读过我《领域驱动设计案例:Tiny Library》这一系列文章的朋友一定听说过Apworks框架,虽然这个框架至今仍未成熟到能够用在真正的系统开发上,但它已经实现了CQRS体系结构模式,已经可以用于软件架构设计的演示上了。从这一讲开始,我将逐步介绍如何采用CQRS架构实现Tiny Library的业务。你可能会听得烦了:又是Tiny Library,能不能换点别的?呵呵,我开始时想做一个简单的论坛出来,不过为了能让读者朋友在经典DDD实践和CQRS实践上做个对比,我决定继续使用Tiny Library的业务。

扩展阅读

在阅读本系列文章之前,如果您对领域驱动设计(DDD)、命令与查询职责分离(CQRS)、事件溯源(Event Sourcing)、事件存储(Event Store)、WCF、ASP.NET MVC等概念和技术不了解的话,请自己先对这些内容做个了解。这里我给出一些链接,希望能对您有所帮助。

系统需求

  • Microsoft .NET Framework 3.5 SP1
  • Microsoft Patterns&Practices Enterprise Library 5.0 (April 2010)
  • Microsoft SQL Express 2008
  • Microsoft Visual Studio 2010 (打开解决方案时需要VS2010)
  • Apworks DDD framework (http://apworks.codeplex.com)。 Apworks的程序集已经被包含在Tiny Library CQRS的源代码包中,您无需单独下载Apworks

注意:目前Apworks框架只能用于演示,仍在开发中,请不要用于实际项目!

安装部署

请按下列步骤安装部署Tiny Library CQRS:

  • 打开Microsoft Visual Studio 2010
  • 打开Server Explorer
  • 邮件单击Data Connections, 选择Create New SQL Server Database option
  • 在Create New SQL Server Database对话框中,在Server name中输入.\SQLEXPRESS,然后在 New database name 中输入 TinyLibraryEventDB
  • 使用上面同样的方法创建另一个数据库,取名为TinyLibraryQueryDB
  • 在Microsoft Visual Studio 2010中,打开 TinyLibraryCQRS 解决方案
  • 在Additions 目录下,执行 TinyLibraryEventDB.sql 和 TinyLibraryQueryDB.sql 脚本
  • 编译整个解决方案
  • 在Solution Explorer上, 邮件单击 TinyLibrary.Services.CommandService.svc 文件, 选择 View in Browser, 这将启动 ASP.NET Development Server,端口号是1421
  • 将TinyLibrary.WebApp项目设置成启动项目,然后按下CTRL+F5以启动应用程序

运行案例应用程序

启动应用程序后,将出现如下界面:

请使用默认的账户登录系统:用户名:daxnet,密码:123456

从下一讲开始,我将详细介绍Tiny Library CQRS的体系结构和设计思路。敬请期待!

CQRS体系结构模式实践案例:Tiny Library:系统架构

写在前面

也许在阅读了上篇文章中我列出的那部分资料后,还是有很多朋友对领域驱动设计不了解。正如上文评论中有网友提到微软西班牙团队也做了一个面向领域的分布式应用系统架构的案例,地址是http://microsoftnlayerapp.codeplex.com/。在这个站点的首页上,又对领域驱动设计做了诠释,我觉得总结的很好,特地将其翻译成中文写在这里,供大家参考:

DDD is much more than this!

We're talking about complex apps…, all their business rules (Domain logic) are points that, in most of the cases, need to be changed quite a lot during the app’s life. So it is critical to be able to change the app and test it in an easy way and independently from infrastructure areas (technology, data stores, data access technolgies, ORMs, etc.). Because of that, it is important to have the Domain/Business Layer (domain logic, entities, etc.) decoupled from the other layers.

Having said so, in our opinion, the best actual architectural style that fits with those requirements is a Domain Oriented N-Layered Architecture which is part of DDD (Domain Driven Design). And this Sample-app is showing a way to implement a Domain Oriented N-Layered Architecture.

But, and this is extremely important, DDD is on the other hand, much more than just a proposed Architecture and patterns. DDD is a way to build apps, a way for the team, to work in projects. According to DDD, the project’s team should work in a specific way, should have direct communication with the Domain Experts (the customer, in many cases). The team should use an ‘Ubiquitous Language’ which has to be the same language/terms used by the domain experts, etc. But, all those ways to work are not part of this Sample-App, because it is process, kind of ALM, so, if you want to really “Do DDD”, you’ll need to read Eric-Evans' book or any other DDD book where they talk about the DDD process, about the project and the team, not just the Architecture and patterns. Architecture and patterns are just a small part of DDD, but, in this case those are the points we are showing here (DDD architecture and patterns). But we want to highlight that DDD is much more than Architecture and design patterns.

DDD - 不仅仅是这些

我们讨论的是复杂的应用系统……,通过这些应用系统的业务规则(领域逻辑)我们可以了解到,在大多数情况下,在应用系统的整个生命周期里,业务规则是可变的一部分。因此,能够很方便地对应用系统进行变更并测试,并使其独立于基础结构层设施(比如:技术、数据存储、数据访问技术、ORM等等),就显得非常重要。正因为如此,我们很有必要将领域/业务层(领域逻辑、实体等等)从其它分层中解耦出来。

话虽如此,根据我们的建议,能够适应上述需求的架构风格就是面向领域的多层架构,但它仅仅是领域驱动设计(DDD)的一个部分。本案例展示了实现面向领域的多层架构的一种方式。但需要注意的,而且非常重要的是,DDD不仅仅只是架构+模式。DDD是开发应用程序的一种方式,是团队在项目中工作的一种方式。根据DDD,项目团队需要以一种特殊的方式进行合作,应该能够直接与领域专家(通常就是客户)进行沟通。整个团队需要使用“通用语言”这一能够被所有人接受的语言,等等。然而,本案例没有包含这些内容,因为这是一种“过程”,一种ALM。所以,如果你真的希望100%实践DDD,你需要阅读Eric Evans写的《领域驱动设计-软件核心复杂性应对之道》一书,或者其它的一些讨论DDD过程的书籍,这些书籍会对DDD过程、项目和团队做比较详细的介绍,而不仅仅是谈论架构和模式。架构和模式仅仅是DDD中很小的一部分,而我们在这里展示的就恰好是这一部分。总之,我们必须强调,DDD不仅仅是架构+模式。

很多网友对什么是DDD有很大争议,我想,看了上面的文字,问题应该大致清楚了。也在此借用这段文字,说明我研究学习DDD实践的意图。言归正传,本文将对Tiny Library CQRS(简称tlibcqrs,下同)的解决方案结构和系统架构做个简要的介绍。

Tiny Library CQRS解决方案结构

从Solution Explorer上可以看到,tlibcqrs包含9个项目,依次为:

  • TinyLibrary.CommandHandlers:命令处理器,负责处理来自客户端的命令请求
  • TinyLibrary.Commands:命令集,其中包含了tlibcqrs所用到的命令,客户端请求将转化为命令,并由命令处理器负责处理执行
  • TinyLibrary.Domain:领域模型,业务逻辑与之前系列文章中的Tiny Library一样,话不多说。不过你会发现,tlibcqrs的领域模型与经典Tiny Library的领域模型在实现上会有区别
  • TinyLibrary.EventHandlers:领域事件处理器,负责处理由领域仓储转发的领域事件
  • TinyLibrary.Events:领域事件集,其中包含了tlibcqrs中所有的领域事件。就是这些领域事件造成了领域模型中实体的状态更新
  • TinyLibrary.QuerObjects:查询对象集,负责数据的查询和传递,它是用户界面的数据映射,是一些DTO
  • TinyLibrary.Services:.NET WCF Services,包含两个endpoint:CommandService和QueryService,为用户界面层提供服务
  • TinyLibrary.Tests:单体测试项目
  • TinyLibrary.WebApp:基于ASP.NET MVC实现的用户界面

仍然说明一下,本案例没有加入太多的cross-cutting基础结构层设施,因此,诸如日志、缓存、异常处理都很弱甚至没有,朋友们不要觉得好奇,这只是一个演示,为了节约时间,我忽略了这部分内容。今后我会将其补上。

系统架构

tlibcqrs的系统架构就是经典的CQRS架构,之前在我的《EntityFramework之领域驱动设计实践【扩展阅读】:CQRS体系结构模式》一文中,我已经给出了整个系统的结构简图,在此再一次贴出来,以表示tlibcqrs的系统架构。

为了让大家看tlibcqrs源代码方便,在此特地以“创建读者”为例,将sequence描述一下。

  • Client通过TinyLibrary.Services.CommandService,向系统发送CreateReader的请求
  • CommandService获得请求,创建RegisterReaderCommand对象,并将其推送到命令总线(Command Bus)
  • 在Command Bus被提交时,它将调用RegisterReaderCommandHandler,以处理RegisterReaderCommand
  • 在RegisterReaderCommandHandler中,创建新的Reader实体,并向其发送ReaderCreatedEvent事件
  • Reader实体捕获到ReaderCreatedEvent后,根据事件中的数据,设置其自身的状态属性,同时将事件记录下来
  • RegisterReaderCommandHandler调用Domain Repository保存新建的Reader实体
  • Domain Repository从Reader实体中读取未提交的事件列表,使用DomainEventStorage将事件保存到外部持久化机制(在tlibcqrs中,是TinyLibraryEventDB这个关系型数据库)
  • 完成事件的保存后,Domain Repository将事件推送到事件总线(Event Bus),然后返回
  • 事件总线接收到事件后,使用消息派发器(Message Dispatcher)将事件发送给相应的事件处理器进行处理
  • ReaderCreatedEventHandler在接到ReaderCreatedEvent事件后,将事件数据保存到查询存储机制中(在tlibcqrs中,是TinyLibraryQueryDB这个关系型数据库),同时返回
  • 下一轮,当用户需要获取Reader信息时,直接通过TinyLibrary.Services.QueryService读取查询存储机制以获得数据

大致可以用下面的UML Sequence Diagram描述这一过程【请单击此处下载XPS文件以查看大图】:

下一讲,我会介绍一些tlibcqrs中的一些新思想,至于CQRS框架的设计,我会在另外的系列文章中详述。

CQRS体系结构模式实践案例:Tiny Library:对象的行为和状态

从结构上看,tlibcqrs项目并不复杂,但对其进行介绍,的确让我感到有点无从着手。还是先从领域模型中的对象的行为和状态谈起吧。

先来谈谈对象状态。据我理解,状态就是一种数据,它用来描述,在某个特定的时间上,这个对象所具有的特质,它将作为对象行为发生的依据和结果。我们平时做设计和编程的时候,尤其是在做数据访问层的时候,特别喜欢一些仅仅包含getter/setter属性的对象,以便调用方能够通过getter获得对象的状态,使用setter设置对象的状态。之前我也说明过,状态并非getter/setter属性,在OOP上,状态表现为“字段”(fields)。现在我们讨论的不是数据访问层的DAO,而是领域模型中的实体。当然,实体也是对象,自然也有状态,不仅仅是状态,实体是参与业务逻辑的重要对象,它还有处理业务逻辑的行为。

现在假设我们有个实体为Customer,它同时也是某个聚合的聚合根,在通常情况下,我们会用下面的形式去定义这个Customer实体(为了简化,省去了对象行为):

当然你不会觉得这样设计有什么太大的问题,事实上在我们平时的开发中,也的确是这么做的,而且非CQRS架构的DDD实践也支持这样的实体模型。于是,我们可以使用下面的代码来更新某个Customer的姓名:

   [TestMethod]    
   public void ChangeCustomerNameTest()    
   {    
       Customer customer = new Customer    
       {    
           Birth = DateTime.Now.AddYears(-20),    
           Email = "daxnet@live.com",    
           FirstName = "dax",    
           LastName = "net",   
            Password = "123456",   
            Username = "daxnet"   
        };   
        using (IRepositoryContext ctx = ObjectContainer.Instance.GetService<IRepositoryContext>())   
        {   
            IRepository<Customer> customerRepository = ctx.GetRepository<Customer>();   
            customerRepository.Add(customer);   
        }   
        ISpecification<Customer> spec = Specification<Customer>.Eval(p => p.Username.Equals("daxnet"));   
        using (IRepositoryContext ctx = ObjectContainer.Instance.GetService<IRepositoryContext>())   
        {   
            IRepository<Customer> customerRepository = ctx.GetRepository<Customer>();   
            var customer2 = customerRepository.Get(spec);   
            Assert.AreEqual(customer.Username, customer2.Username);   
        
            customer2.FirstName = "qingyang";   
            customer2.LastName = "chen";   
            customerRepository.Update(customer2);   
        }   
        using (IRepositoryContext ctx = ObjectContainer.Instance.GetService<IRepositoryContext>())   
        {   
            IRepository<Customer> customerRepository = ctx.GetRepository<Customer>();   
            var customer3 = customerRepository.Get(spec);   
            Assert.AreEqual("qingyang", customer3.FirstName);   
            Assert.AreEqual("chen", customer3.LastName);   
        }   
    }

在上面代码段的25行和26行,我们直接设置了Customer的姓名,然后在27行调用仓储进行实体更新。一切都非常顺利。今后,实体中的属性,就成为了我们实现业务逻辑和实体行为的依据。然而,CQRS体系结构模式的实践就与这种方式大相径庭。CQRS是一种事件驱动的架构(Event Driven Architecture,EDA),它的设计与实践需要遵循事件驱动的基本特征。

事件驱动架构下的对象状态

事件驱动架构是一种体系结构模式,它对事件的整个生命周期进行检测、跟踪和管理,并对事件的产生与发展做出反应。“事件”,可以定义为“造成状态变化的信号”。比如:当用户买了一辆轿车,那么,这辆车的状态就从“等待销售”转变为“已出售”。汽车销售系统就会将此作为一种事件将其发布到事件总线,以便应用程序的其它系统或组件能够订阅到这个事件并做进一步的处理(请参见维基百科:http://en.wikipedia.org/wiki/Event_driven_architecture)。

从现在开始,我们需要重新认识对象状态的变化。由于EDA的引入,对象状态只能通过事件的发生而产生变化,外界不能无缘无故地对其进行改变(就像上面的例子一样)。这样做的好处是:我们不仅能够知道对象的当前状态,而且还能知道,到底发生了哪些事情,才使对象变成现在这副“模样”。当某一事件发生时,对象捕获到这一事件并对其进行处理,而在处理的过程中再根据事件的类型和数据来改变自己的状态。由于通常情况下,这样的处理过程是对象的内部行为,因此,我们也就无需将更改状态的接口暴露给外部。在理解了这部分内容后,我们的Customer对象的设计就需要做出修改,变成下面这种形式(为了讨论方便,在此将Customer实体命名为SourcedCustomer,意为支持事件溯源):

    public class SourcedCustomer : SourcedAggregateRoot    
    {    
        public virtual string Username { get; private set; }    
        public virtual string Password { get; private set; }    
        public virtual string FirstName { get; private set; }    
        public virtual string LastName { get; private set; }    
        public virtual string Email { get; private set; }    
        public virtual DateTime Birth { get; private set; }    
    }

将setter定义为private,以防止外界直接修改对象状态。对于某些外部也不关心的状态,我们甚至连getter都可以省去(也就是不需要再实现为property了),取而代之的是一个private的字段。比如:

   public class SourcedCustomer : SourcedAggregateRoot    
   {    
       private DateTime dayOfBirth;    
       // ....    
   }

对象的行为导致状态变化

现在回到tlibcqrs项目,让我们看看TinyLibrary.Domain下Book实体的实现方式。它的状态是一系列的public getter和private setter的自动实现的属性。Book实体状态的改变,是通过其行为实现的。当某个行为被外界调用时,行为本身会产生一个事件,而对象本身又去处理这个事件,从而导致状态变化。

    public class Book : SourcedAggregateRoot    
    {    
        public string Title { get; private set; }    
        public string Publisher { get; private set; }    
        public DateTime PubDate { get; private set; }    
        public string ISBN { get; private set; }    
        public int Pages { get; private set; }    
        public bool Lent { get; private set; }    
       
        public Book() : base() {  }   
        public Book(long id) : base(id) {  }   
        
        public static Book Create(string title, string publisher, DateTime pubDate, string isbn, int pages, 
bool lent)
        {   
            Book book = new Book();  
            book.RaiseEvent<BookCreatedEvent>(new BookCreatedEvent   
            {   
                Title = title,   
                Publisher = publisher,   
                PubDate = pubDate,   
                ISBN = isbn,   
                Pages = pages,   
                Lent = lent   
            });   
            return book;   
        }   
        
        public static Book Create(long id, string title, string publisher, DateTime pubDate, string isbn, 
int pages,bool lent)   
        {   
            Book book = new Book(id);   
            book.RaiseEvent<BookCreatedEvent>(new BookCreatedEvent   
            {   
                Title = title,   
                Publisher = publisher,   
                PubDate = pubDate,   
                ISBN = isbn,   
                Pages = pages,   
                Lent = lent   
            });   
            return book;   
        }   
        
        public void LendTo(Reader reader)   
       {   
            this.RaiseEvent<BookLentEvent>(new BookLentEvent { ReaderId = reader.Id, LentDate = DateTime.Now });   
        }   
        
        public void ReturnBy(Reader reader)   
        {   
            this.RaiseEvent<BookGetReturnedEvent>(new BookGetReturnedEvent 
               { ReaderId = reader.Id, ReturnedDate = DateTime.Now });   
        }   
        
        #region Domain Event Handlers   
        [Handles(typeof(BookCreatedEvent))]   
        private void OnBookCreated(BookCreatedEvent evnt)   
        {   
            this.Title = evnt.Title;   
            this.Publisher = evnt.Publisher;   
            this.PubDate = evnt.PubDate;   
            this.ISBN = evnt.ISBN;   
            this.Pages = evnt.Pages;   
            this.Lent = evnt.Lent;   
        }   
        [Handles(typeof(BookLentEvent))]   
        private void OnBookLent(BookLentEvent evnt)   
        {   
            this.Lent = true;   
        }   
        [Handles(typeof(BookGetReturnedEvent))]   
        private void OnBookReturnedBack(BookGetReturnedEvent evnt)   
        {   
            this.Lent = false;   
        }   
        #endregion   
    }

上面的代码就是TinyLibrary.Domain.Book实体,我们可以看到,在Book被创建的时候,会产生BookCreatedEvent事件,当Book被借出时,会产生BookLentEvent事件,当Book被归还时,又会产生BookGetReturnedEvent事件。而这些事件会被OnBookCreated、OnBookLent和OnBookReturnedBack私有方法捕获,从而引起对象状态的变化。

不仅如此,SourcedAggregateRoot基类会将这些事件记录下来。领域仓储(Domain Repository)在保存聚合的时候,就只需要保存这一系列事件就可以了。这也是为什么在tlibcqrs项目中有一个TinyLibraryEventDB的数据库,而这个数据库却只有一张DomainEvents数据表的原因。由于实体本身能够通过捕获并处理事件来恢复状态,因此,通过事件回放即可重塑实体。当然,在领域仓储保存聚合的同时,这些事件也会被推送到事件总线,以便系统的其它部分能够对这些事件进行处理。

有关领域仓储(Domain Repository)和事件存储(Event Store)相关的内容和疑问,将在下一讲中进行讲解,敬请期待!

CQRS体系结构模式实践案例:Tiny Library:领域仓储与事件存储

领域仓储(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.由于查询部分的分离,领域仓储仅存两种操作:将聚合保存到事件存储以及从事件存储还原对象。与之对应的操作大致可以表示成下面的接口(该部分代码摘录自Apworks Application Development Framework):

    public interface IDomainRepository : IUnitOfWork, IDisposable    
    {    
        TAggregateRoot Get<TAggregateRoot>(long id)    
            where TAggregateRoot : class, ISourcedAggregateRoot, new();    
         
        void Save<TAggregateRoot>(TAggregateRoot aggregateRoot)    
            where TAggregateRoot : class, ISourcedAggregateRoot, new();    
    } 

1.整个Domain Model只有一个数据源:事件存储(Event Store),用来保存所有发生在聚合上的领域事件。这个Event Store具体如何设计,可以根据应用系统的需求来决定,但总归是非常的简单,甚至于仅用一张关系型数据库的数据表就可以实现。对于采用关系型数据库实现的事件存储,由于数据表数量很少,而且之间的关系变得非常简单,于是ORM就可以省略,直接采用Direct SQL实现;如果不采用关系型数据库作为事件存储,那可以选择的范围就更大了:各种NoSQL数据库、对象数据库、内存数据库等等。就关系型数据库而言,我们可以对事件存储所使用的数据表做如下的设计:

目前,Tiny Library CQRS赖以生存的开发框架Apworks,仅提供支持SQL Server的Event Store设计(Apworks当前版本:Alpha,v1.0.4016.23016)

2.如果事件存储采用的是关系型数据库,领域仓储对事件存储,原则上也只有类似如下两种操作:

    // 查询事件存储    
    SELECT * FROM [Events] WHERE AggregateId=xxx ORDER BY Version    
         
    // 向事件存储保存事件    
    INSERT INTO [Events] ([AggregateId], [Timestamp], [Version], [Data]) VALUES (...)

当然,在实际应用中,领域仓储与事件存储的实现并没有那么简单。原因可以通过如下几个疑问进行了解:

  • 领域仓储的设计中,没有提到从事件存储中删除事件数据,时间一长,岂不是事件存储会变得很大?
    没错!领域仓储从来不会从事件存储中删除数据,即使是客户程序请求删除某个领域对象,这一操作也同样会产生一个事件(比如:ReaderDeletedEvent)并保存在事件存储中。这样做的理由来自于Event Sourcing所带来的一种数据分析与跟踪的可能性:Event Audit。它允许你将你的领域模型还原到任何时间点,然后通过事件重放(replay)来诊断你的模型数据。不仅如此,你还可以利用这些保存的数据重新搭建你的测试环境,用来对对象数据进行测试。当然,目前大部分系统可能用不到这样的Event Audit的功能,那么,在引入“快照”的情况下,你可以从事件存储数据库中定期地删除数据。然而,这是另外一种“退化”的CQRS设计,也同样是合理的,不过这不是我们讨论的范围。我们要讨论的是,时间一长,事件存储变得巨大怎么办?
    CQRS架构社区中有一句非常有意思的话,就是:Storage is cheap,data is valuable(存储是廉价的,而数据是有价值的)。通常,都是通过大容量存储备份以及数据归档来解决这样的问题:对于较早的事件数据,我们选用高速而昂贵的存储介质进行备份,而对于更早的事件数据,则可以采用低速而便宜的存储介质进行归档,综合采用两种不同的方案以使得事件存储端“性价比”达到最高。当然,这样的策略同样需要“快照”的支持
  • 某些聚合的生命周期可能很长,于是就会在它们身上产生大量的事件数据,当领域仓储重建这些聚合的时候,需要把大量的事件依次地“应用”在这些聚合上,岂不是会花很多时间?
    在此,我们通过引入“快照”的概念来解决这个问题。在系统中,可以根据一定的“快照策略”来确定何时应该对聚合进行“快照”。每当这个快照策略的条件符合,系统就会对聚合做一次快照,并将快照数据记录在事件存储中。比如:我们可以指定,每n个事件发生时,就对聚合做一次快照,于是,当我们需要获得第n+3个事件发生时,该聚合的状态的时候,就只需要直接从事件存储中读取第n个事件发生时,聚合的快照,然后再依次将n+1、n+2、n+3个事件应用到聚合即可。这样就大大缩短了重建聚合所需的时间,也使得上面第一个问题中归档的实现成为可能
    在Apworks应用开发框架中,目前版本(Alpha,v1.0.4016.23016)对快照的支持是采用的GoF的memento模式,而对快照策略的支持就显得非常简单:仅仅是通过Apworks.Events.Storage.IDomainEventStorage.CanCreateOrUpdateSnapshot方法进行定义的。在Apworks.Events.Storage.SqlDomainEventStorage类中,实现了这个方法,并指定每当第1000个领域事件发生时,对聚合做一次快照。如果你打算继续采用SQL Server作为事件存储,并打算重新定制快照策略,请新建一个类并继承Apworks.Events.Storage.SqlDomainEventStorage类,然后重写CanCreateOrUpdateSnapshot方法;如果你打算采用其它的介质作为事件存储,则请自行实现Apworks.Events.Storage.IDomainEventStorage接口
  • 在保存聚合的时候,领域仓储不仅需要将事件保存到事件存储,而且还需要将事件推送到事件总线上,这样做从技术上很难保证操作的原子性,换句话说,会不会造成数据的不一致性?
    是的,这就是所谓的“两次提交”(Two-Phase Commit, TPC)操作。在设计中应该避免TPC的出现,因为在两次提交之间会发生很多事情,如果不能保证操作的原子性,也就无法保证数据的一致性。对于CQRS体系结构的应用系统而言,这是致命的。目前在事件存储部分,避免TPC有两种方案:A.将事件存储整合到事件总线;B.将事件总线整合到事件存储。总之,思想只有一个:就是采用同一个持久化机制来整合存储部分与总线部分。有关TPC的深入研究,我会在后续的扩展话题中讨论。目前版本的Apworks(Alpha,v1.0.4016.23016)不提供对TPC的支持

现在,我们再来看看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子目录中找到这个文件。

   <?xml version="1.0" encoding="UTF-8"?>    
    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"     
               elementFormDefault="qualified"     
               attributeFormDefault="unqualified">    
        <xs:element name="StorageMappingSchema">    
            <xs:annotation>    
                <xs:documentation/>    
           </xs:annotation>    
            <xs:complexType>   
                <xs:sequence minOccurs="0">   
                    <xs:element ref="DataTypes"/>   
                </xs:sequence>   
            </xs:complexType>   
        </xs:element>   
        <xs:element name="DataTypes">   
            <xs:complexType>   
                <xs:sequence minOccurs="0" maxOccurs="unbounded">   
                    <xs:element ref="DataType"/>   
                </xs:sequence>   
            </xs:complexType>   
        </xs:element>   
        <xs:element name="DataType">   
            <xs:complexType>   
                <xs:sequence minOccurs="0">   
                    <xs:element ref="Properties"/>   
                </xs:sequence>   
                <xs:attribute name="FullName" type="xs:string" use="required"/>   
                <xs:attribute name="MapTo" type="xs:string" use="required"/>   
            </xs:complexType>   
        </xs:element>   
        <xs:element name="Properties">   
            <xs:complexType>   
                <xs:sequence minOccurs="0" maxOccurs="unbounded">   
                    <xs:element ref="Property"/>   
                </xs:sequence>   
            </xs:complexType>   
        </xs:element>   
        <xs:element name="Property">   
            <xs:complexType>   
                <xs:attribute name="Name" type="xs:string" use="required"/>   
                <xs:attribute name="MapTo" type="xs:string" use="required"/>   
                <xs:attribute name="Identity" type="xs:boolean" use="optional"/>   
                <xs:attribute name="AutoGenerate" type="xs:boolean" use="optional"/>   
            </xs:complexType>   
        </xs:element>   
    </xs:schema>

最后,在此给出Apworks应用开发框架中基于SQL Server的Event Store的类关系图,供大家参考。为了节省版面空间,此图中隐藏了类中的属性与方法定义,有兴趣的朋友可以到Apworks的站点http://apworks.codeplex.com上查看具体的代码实现。


 
分享到
 
 


专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS


面向应用的架构设计实践
单元测试+重构+设计模式
软件架构师—高级实践
软件架构设计方法、案例与实践
嵌入式软件架构设计—高级实践
SOA体系结构实践


锐安科技 软件架构设计方法
成都 嵌入式软件架构设计
上海汽车 嵌入式软件架构设计
北京 软件架构设计
上海 软件架构设计案例与实践
北京 架构设计方法案例与实践
深圳 架构设计方法案例与实践
嵌入式软件架构设计—高级实践
更多...