別のクエリコマンド義務について[ターン](CQRS)モード

オリジナルリンク:https://www.cnblogs.com/yangecnu/p/Introduction-CQRS.html

従来の3層システムでは、通常は変更したり、データアクセス層を介してクエリデータは、典型的に変更し、同じエンティティを照会するために使用されます。いくつかのビジネスロジックでは、単純なシステムでは、すべての問題を持っていない可能性がありますが、システム・ロジックが複雑になると、ユーザーが増加し、この設計は、いくつかのパフォーマンスの問題になります。あなたは、DB上で、いくつかの個別の読み取りと書き込みの設計を行うことができますが、読み込みと書き込みで一緒に混合した場合、それはまだビジネスでいくつかの問題になりますが。

この記事では、業務のモード(コマンドクエリ責任分掌、CQRS)のコマンドクエリ分離を説明し、(システムステータスが変更され、変更、削除、追加、コマンド)サービスからの分離モードを変更するとクエリ(クエリ、調査、しませんシステムステータス)が動作を変更します。ロジックより明確と異なる部分を対象と最適化のための簡単なようにします。まず、CRUDの問題の伝統的な方法の簡単な紹介は、その後、CQRSモードを導入し、最後に簡単なWeb日記システムにどのようにCQRSモードを示しています。読み込みと書き込みの話をするには、まず私たちは、伝統的なCRUDの問題を見てください。

CRUDメソッドを発行

以前の管理システムでは、コマンドそれが一般に(これらのオブジェクトは、DBテーブルにマッピングされている)エンティティ・オブジェクトリポジトリ・データ・アクセス・レイヤーで使用され、クエリ(照会)(コマンドは、通常、データ、動作DBを更新するために使用されます)これらのエンティティは、SQLServerのまたは複数のテーブル内のデータの行であってもよいです。

典型的には、システムのすべてのエンティティオブジェクトを追加、削除、変更、(CRUD)を行う。DBの データは、データアクセス層、プレゼンテーション層によって取得し、データDTO転送オブジェクトを通過しました。あるいは、ユーザは、モデルへのDTOオブジェクトデータによって、データを更新する必要があり、その後、バックデータベースに書き込まれたデータアクセス層を介して、全てのインタラクティブシステムは、ストレージおよびデータクエリに関連していると、データ駆動型であると考えることができる(データ駆動型)の以下に示すとおり

 伝統的なCRUDのアーキテクチャ

より単純なシステムのいくつかのために、そのようなCRUDの設計アプローチの使用は、要件を満たすことができます。そのようなORMのような特定の生成ツールに、コードの一部によって非常に容易にかつ迅速に機能を実現することができます。

しかし、伝統的なCRUDメソッドは、いくつかの問題があります

  • データベースが読み取りと書き込みのための同じオブジェクトの実体は、そのような編集は、個々のフィールドを更新することだけが必要な場合がありますが、あなたが実際に更新する必要はありませんいくつかのフィールドに、オブジェクト全体を着用する必要がある場合など、ほとんどの場合、には、粗すぎるかもしれ使用します。場合は、プレゼンテーション層での問合せは、唯一の個々のフィールドに必要がありますが、全体のエンティティオブジェクトを照会して返す必要があります。
  • 同じデータの読み取りに同じエンティティの件名を使用し、書き込み時間、リソースの競合が発生する可能性があり、問題が頻繁にデータを書き込む際に、あなたがロックする必要がある、処理されるようにロックします。読み出しデータが必要な場合はダーティリード許可するかどうかを決定します。ロジック及びシステムの複雑さは、システムのスループットが増加し、影響を与えるようにします。
  • 同期は、直接同時にアクセスされるデータが大量の場合にはパフォーマンスと応答に影響を与えることができるデータベースと対話し、パフォーマンスのボトルネックをもたらすことができます。
  • 同じエンティティオブジェクトは、読み取りに使用し、安全と権利管理がより複雑になりますためので、書き込み操作をされますので。

、された非常に重要な問題があり読み、システムの周波数よりも書くこと、読むだけで一般的なように、書き込みまたはバイアスする傾向がされて発見し、修正の時間複雑でデータ構造は、システムはまた、必要の構造設計では、同じではありませんこの問題を考えます。解決策は、私たちはしばしば読み、分離を書くためにデータベースを使用しています。データベースクエリ(操作を選択)からの処理は、データベースの変更をクラスタに同期を引き起こすために使用されるトランザクション操作を複製されるように、操作(削除、挿入、更新)変更操作、削除、主が増加したトランザクションデータベースを処理してみましょうデータベースから。このプロセスは、別個の読み取りおよび書き込みDBの観点から、しかし業務システムから、または静止読み取りと一緒に書き込み上方に配置されています。彼らは、同じエンティティオブジェクトで使用されています。

ビジネスは、それが業務のクエリモードの分離を導入する次のコマンドで、分離を読み書きすることになるでしょう。

2 CQRSは何ですか

最初Betrandマイヤー(エッフェル言語の父、からCQRS オープン-クローズド原則 OCPの作者)における  オブジェクト指向ソフトウェア構築  の本の中で述べた種類の  クエリ分離コマンドコマンドクエリ分離コンセプト、CQS)されます。基本的な考え方は、オブジェクトの任意の方法は、2つのカテゴリに分けることができることです。

  • コマンド(コマンド):何の結果は(ボイド)が返されませんが、オブジェクトの状態を変更します。
  • クエリ(クエリ):結果を返すが、オブジェクト、システム上の副作用なしの状態を変更しません。

CQSの考え方によれば、任意の方法は、次のような二つの部分のコマンド及びクエリに分割することができます。

プライベートint型私= 0;
プライベートint型の増加(int値)
{
    I + =値;
    私は返します。
}

この方法で、私たちはクエリを実行すると同時に、追加することで、変数iは、コマンドを実行し、次のように、つまり、クエリは、二つの方法のコマンドとクエリに分割することができCQSの考え方によれば、iの値を返します。 :

プライベートボイドIncreaseCommand(int値)
{
    I + =値;
}
プライベートint型QueryValue()
{
    私は返します。
}

操作とクエリの分離をよりよく理解するためにシステムを変更しますどのような行動状態、私たちはより良いオブジェクトの詳細を把握することができます。もちろん、CQSはまた、このようなマルチスレッドコードなど、いくつかの欠点は、状況に対処する必要がありました。

CQRS CQSは更なる改善に単純なモードパターンです。それはでグレッグ・ヤングで構成!CQRS、タスクベースのUI、イベント AGHソーシング 提案この記事を。「CQRSは、単にちょうど2つのオブジェクトに分割する前にオブジェクトを作成する必要があります、この方法は、分離に基づいており(CQSと、この定義と一致)、またはこの原則に与えられたクエリを実行するためのコマンドを実行します。」

CQRSは、クエリおよび更新プロセスで使用されるデータモデルは同じではないことを意味し、分離された別個のインタフェースデータクエリ操作(クエリ)とデータ変更操作(コマンド)を使用します。ロジックの分離株のような読み取りと書き込み。

基本的なアーキテクチャCQRS

そしてリードを分離した後CQRS、別個のリードを使用して関数を作成し、操作は、パフォーマンス、スケーラビリティ、およびセキュリティを向上させるためにデータに対して実行されてもよい書き込みます。図は次のとおりです。

別の読み取りおよび書き込み店とCQRSアーキテクチャ

CUDプライマリ・データベース処理、Rからのデータベース処理は、ライブラリーの構造からメインバンクの構造と全く同じにすることができ、それは読み取り専用のクエリのために使用される一次ライブラリーと異なっていてもよいです。拡張されたクエリのサイズに基づいて銀行の数から数で行うことができる、ビジネスロジックに、別のテーマに応じてライブラリからマスターライブラリから分割することができます。ライブラリからもとして実装することができReportingDatabaseビジネスによると、主記憶リポジトリから一連のクエリにレポートを生成するために必要なデータを抽出するためのクエリを必要とします。

reportingDatabase

ReportingDatabaseを使用してのいくつかの利点は、通常のクエリをより簡単かつ効率的に行うことができます。

  • ReportingDatabase構造とデータテーブルは、一般的なクエリのために設計されます。
  • 通常簡素化と効率化、プライマリ・データベース情報で使用されていないデータのいくつかは、ReportingDatabaseを保存することができないように、ユニオンクエリなどの操作に参加したクエリを削減するために必要ないくつかの冗長性を格納し、正規化されたデータベースを行くReportingDatabase。
  • 再構成は、データベースの動作を変更することなく、ReportingDatabaseを最適化することができます。
  • データベースへのクエリは、データベースを操作するための任意の圧力をもたらすことはありませんReportingDatabase。
  • あなたは、異なるクエリの異なるReportingDatabaseライブラリを作成することができます。

当然这也有一些缺点,比如从库数据的更新。如果使用SQLServer,本身也提供了一些如故障转移和复制机制来方便部署。

三 什么时候可以考虑CQRS

CQRS模式有一些优点:

  1. 分工明确,可以负责不同的部分
  2. 将业务上的命令和查询的职责分离能够提高系统的性能、可扩展性和安全性。并且在系统的演化中能够保持高度的灵活性,能够防止出现CRUD模式中,对查询或者修改中的某一方进行改动,导致另一方出现问题的情况。
  3. 逻辑清晰,能够看到系统中的那些行为或者操作导致了系统的状态变化。
  4. 可以从数据驱动(Data-Driven) 转到任务驱动(Task-Driven)以及事件驱动(Event-Driven).

在下场景中,可以考虑使用CQRS模式:

  1. 当在业务逻辑层有很多操作需要相同的实体或者对象进行操作的时候。CQRS使得我们可以对读和写定义不同的实体和方法,从而可以减少或者避免对某一方面的更改造成冲突
  2. 对于一些基于任务的用户交互系统,通常这类系统会引导用户通过一系列复杂的步骤和操作,通常会需要一些复杂的领域模型,并且整个团队已经熟悉领域驱动设计技术。写模型有很多和业务逻辑相关的命令操作的堆,输入验证,业务逻辑验证来保证数据的一致性。读模型没有业务逻辑以及验证堆,仅仅是返回DTO对象为视图模型提供数据。读模型最终和写模型相一致。
  3. 适用于一些需要对查询性能和写入性能分开进行优化的系统,尤其是读/写比非常高的系统,横向扩展是必须的。比如,在很多系统中读操作的请求时远大于写操作。为适应这种场景,可以考虑将写模型抽离出来单独扩展,而将写模型运行在一个或者少数几个实例上。少量的写模型实例能够减少合并冲突发生的情况
  4. 适用于一些团队中,一些有经验的开发者可以关注复杂的领域模型,这些用到写操作,而另一些经验较少的开发者可以关注用户界面上的读模型。
  5. 对于系统在将来会随着时间不段演化,有可能会包含不同版本的模型,或者业务规则经常变化的系统
  6. 需要和其他系统整合,特别是需要和事件溯源Event Sourcing进行整合的系统,这样子系统的临时异常不会影响整个系统的其他部分。

但是在以下场景中,可能不适宜使用CQRS:

  1. 领域模型或者业务逻辑比较简单,这种情况下使用CQRS会把系统搞复杂。
  2. 对于简单的,CRUD模式的用户界面以及与之相关的数据访问操作已经足够的话,没必要使用CQRS,这些都是一个简单的对数据进行增删改查。
  3. 不适合在整个系统中到处使用该模式。在整个数据管理场景中的特定模块中CQRS可能比较有用。但是在有些地方使用CQRS会增加系统不必要的复杂性。

四 CQRS与Event Sourcing的关系

在CQRS中,查询方面,直接通过方法查询数据库,然后通过DTO将数据返回。在操作(Command)方面,是通过发送Command实现,由CommandBus处理特定的Command,然后由Command将特定的Event发布到EventBus上,然后EventBus使用特定的Handler来处理事件,执行一些诸如,修改,删除,更新等操作。这里,所有与Command相关的操作都通过Event实现。这样我们可以通过记录Event来记录系统的运行历史记录,并且能够方便的回滚到某一历史状态。Event Sourcing就是用来进行存储和管理事件的。这里不展开介绍。

五 CQRS的简单实现

CQRS模式在思想上比较简单,但是实现上还是有些复杂。它涉及到DDD,以及Event Sourcing,这里使用codeproject上的 Introduction to CQRS 这篇文章的例子来说明CQRS模式。这个例子是一个简单的在线记日志(Diary)系统,实现了日志的增删改查功能。整体结构如下:

CQRS

上图很清晰的说明了CQRS在读写方面的分离,在读方面,通过QueryFacade到数据库里去读取数据,这个库有可能是ReportingDB。在写方面,比较复杂,操作通过Command发送到CommandBus上,然后特定的CommandHandler处理请求,产生对应的Event,将Eevnt持久化后,通过EventBus特定的EevntHandler对数据库进行修改等操作。

例子代码可以到codeproject上下载,整体结构如下:

DiaryCQRSプロジェクト

由三个项目构成,Diary.CQRS包含了所有的Domain和消息对象。Configuration通过使用一个名为StructMap的IOC来初始化一些变量方便Web调用,Web是一个简单的MVC3项目,在Controller中有与CQRS交互的代码。

下面分别看Query和Command方面的实现:

Query方向的实现

查询方面很简单,日志列表和明细获取就是简单的查询。下面先看列表查询部分的代码。

public ActionResult Index()
{
    ViewBag.Model = ServiceLocator.ReportDatabase.GetItems();
    return View();
}

public ActionResult Edit(Guid id)
{
    var item = ServiceLocator.ReportDatabase.GetById(id);
    var model = new DiaryItemDto()
    {
        Description = item.Description,
        From = item.From,
        Id = item.Id,
        Title = item.Title,
        To = item.To,
        Version = item.Version
    };
    return View(model);
}

ReportDatabase的GetItems和GetById(id)方法就是简单的查询,从命名可以看出他是ReportDatabase。

public class ReportDatabase : IReportDatabase
{
    static List<DiaryItemDto> items = new List<DiaryItemDto>();

    public DiaryItemDto GetById(Guid id)
    {
        return items.Where(a => a.Id == id).FirstOrDefault();
    }

    public void Add(DiaryItemDto item)
    {
        items.Add(item);
    }

    public void Delete(Guid id)
    {
        items.RemoveAll(i => i.Id == id);
    }

    public List<DiaryItemDto> GetItems()
    {
        return items;
    } 
}

ReportDataBase只是在内部维护了一个List的DiaryItemDto列表。在使用的时候,是通过IRepositoryDatabase对其进行操作的,这样便于mock代码。

Query方面的代码很简单。在实际的应用中,这一块就是直接对DB进行查询,然后通过DTO对象返回,这个DB可能是应对特定场景的报表数据库,这样可以提升查询性能。

下面来看Command方向的实现:

Command方向的实现

Command的实现比较复杂,下面以简单的创建一个新的日志来说明。

在MVC的Control中,可以看到Add的Controller中只调用了一句话:

[HttpPost]
public ActionResult Add(DiaryItemDto item)
{
    ServiceLocator.CommandBus.Send(new CreateItemCommand(Guid.NewGuid(), item.Title, item.Description, -1, item.From, item.To));

    return RedirectToAction("Index");
}

首先声明了一个CreateItemCommand,这个Command只是保存了一些必要的信息。

public class CreateItemCommand:Command
{
    public string Title { get; internal set; }
    public string Description { get;internal set; }
    public DateTime From { get; internal set; }
    public DateTime To { get; internal set; }

    public CreateItemCommand(Guid aggregateId, string title, 
        string description,int version,DateTime from, DateTime to)
        : base(aggregateId,version)
    {
        Title = title;
        Description = description;
        From = from;
        To = to;
    }
}

然后将Command发送到了CommandBus上,其实就是让CommandBus来选择合适的CommandHandler来处理。

public class CommandBus:ICommandBus
{
    private readonly ICommandHandlerFactory _commandHandlerFactory;

    public CommandBus(ICommandHandlerFactory commandHandlerFactory)
    {
        _commandHandlerFactory = commandHandlerFactory;
    }

    public void Send<T>(T command) where T : Command
    {
        var handler = _commandHandlerFactory.GetHandler<T>();
        if (handler != null)
        {
            handler.Execute(command);
        }
        else
        {
            throw new UnregisteredDomainCommandException("no handler registered");
        }
    }        
}

这个里面需要值得注意的是CommandHandlerFactory这个类型的GetHandler方法,他接受一个类型为T的泛型,这里就是我们之前传入的CreateItemCommand。来看他的GetHandler方法。

public class StructureMapCommandHandlerFactory : ICommandHandlerFactory
{
    public ICommandHandler<T> GetHandler<T>() where T : Command
    {
        var handlers = GetHandlerTypes<T>().ToList();

        var cmdHandler = handlers.Select(handler => 
            (ICommandHandler<T>)ObjectFactory.GetInstance(handler)).FirstOrDefault();
            
        return cmdHandler;
    }
        
    private IEnumerable<Type> GetHandlerTypes<T>() where T : Command
    {
        var handlers = typeof(ICommandHandler<>).Assembly.GetExportedTypes()
            .Where(x => x.GetInterfaces()
                .Any(a => a.IsGenericType && a.GetGenericTypeDefinition() == typeof(ICommandHandler<>) ))
                .Where(h=>h.GetInterfaces()
                    .Any(ii=>ii.GetGenericArguments()
                        .Any(aa=>aa==typeof(T)))).ToList();

           
        return handlers;
    }

}

这里可以看到,他首先查找当前的程序集中(ICommandHandler)所在的程序集中的所有的实现了ICommandHandler的接口的类型,然后在所有的类型找查找实现了该泛型接口并且泛型的类型参数类型为T类型的所有类型。以上面的代码为例,就是要找出实现了ICommandHandler<CreateItemCommand>接口的类型。可以看到就是CreateItemCommandHandler类型。

public class CreateItemCommandHandler : ICommandHandler<CreateItemCommand>
{
    private IRepository<DiaryItem> _repository;

    public CreateItemCommandHandler(IRepository<DiaryItem> repository)
    {
        _repository = repository;
    }

    public void Execute(CreateItemCommand command)
    {
        if (command == null)
        {
            throw new ArgumentNullException("command");
        }
        if (_repository == null)
        {
            throw new InvalidOperationException("Repository is not initialized.");
        }
        var aggregate = new DiaryItem(command.Id, command.Title, command.Description, command.From, command.To);
        aggregate.Version = -1;
        _repository.Save(aggregate, aggregate.Version);
    }
}

找到之后然后使用IOC实例化了该对象返回。

现在CommandBus中,找到了处理特定Command的Handler。然后执行该类型的Execute方法。

可以看到在该类型中实例化了一个名为aggregate的DiaryItem对象。这个和我们之前查询所用到的DiaryItemDto有所不同,这个一个领域对象,里面包含了一系列事件。

public class DiaryItem : AggregateRoot, 
    IHandle<ItemCreatedEvent>,
    IHandle<ItemRenamedEvent>,
    IHandle<ItemFromChangedEvent>, 
    IHandle<ItemToChangedEvent>,
    IHandle<ItemDescriptionChangedEvent>,
    IOriginator
{
    public string Title { get; set; }

    public DateTime From { get; set; }
    public DateTime To { get; set; }
    public string Description { get; set; }

    public DiaryItem()
    {
            
    }

    public DiaryItem(Guid id,string title, string description,  DateTime from, DateTime to)
    {
        ApplyChange(new ItemCreatedEvent(id, title,description, from, to));
    }

    public void ChangeTitle(string title)
    {
        ApplyChange(new ItemRenamedEvent(Id, title));
    }

    public void Handle(ItemCreatedEvent e)
    {
        Title = e.Title;
        From = e.From;
        To = e.To;
        Id = e.AggregateId;
        Description = e.Description;
        Version = e.Version;
    }

    public void Handle(ItemRenamedEvent e)
    {
        Title = e.Title;
    }
    ...
}

ItemCreatedEvent 事件的定义如下,其实就是用来存储传输过程中需要用到的数据。

public class ItemCreatedEvent:Event
{
    public string Title { get; internal set; }
    public DateTime From { get; internal set; }
    public DateTime To { get; internal set; }
    public string Description { get;internal set; }

    public ItemCreatedEvent(Guid aggregateId, string title ,
        string description, DateTime from, DateTime to)
    {
        AggregateId = aggregateId;
        Title = title;
        From = from;
        To = to;
        Description = description;
    }
}

可以看到在Domain对象中,除了定义基本的字段外,还定义了一些相应的事件,比如在构造函数中,实际上是发起了一个名为ItemCreateEvent的事件,同时还定义了处理时间的逻辑,这些逻辑都放在名为Handle的接口方法发,例如ItemCerateEvent的处理方法为Handle(ItemCreateEvent)方法。

ApplyChange方法在AggregateRoot对象中,他是聚集根,这是DDD中的概念。通过这个根可以串起所有对象。 该类实现了IEventProvider接口,他保存了所有在_changes中的所有没有提交的变更,其中的ApplyChange的用来为特定的Event查找Eventhandler的方法:

public abstract class AggregateRoot : IEventProvider
{
    private readonly List<Event> _changes;

    public Guid Id { get; internal set; }
    public int Version { get; internal set; }
    public int EventVersion { get; protected set; }

    protected AggregateRoot()
    {
        _changes = new List<Event>();
    }

    public IEnumerable<Event> GetUncommittedChanges()
    {
        return _changes;
    }

    public void MarkChangesAsCommitted()
    {
        _changes.Clear();
    }

    public void LoadsFromHistory(IEnumerable<Event> history)
    {
        foreach (var e in history) ApplyChange(e, false);
        Version = history.Last().Version;
        EventVersion = Version;
    }

    protected void ApplyChange(Event @event)
    {
        ApplyChange(@event, true);
    }

    private void ApplyChange(Event @event, bool isNew)
    {
        dynamic d = this;

        d.Handle(Converter.ChangeTo(@event, @event.GetType()));
        if (isNew)
        {
            _changes.Add(@event);
        }
    }
}

在ApplyChange的实现中,this其实就是对应的实现了AggregateRoot的DiaryItem的Domain对象,调用的Handle方法就是我们之前在DiaryItem中定义的行为。然后将该event保存在内部的未提交的事件列表中。相关的信息及事件都保存在了定义的aggregate对象中并返回。

然后Command继续执行,然后调用了_repository.Save(aggregate, aggregate.Version);这个方法。先看这个Repository对象。

public class Repository<T> : IRepository<T> where T : AggregateRoot, new()
{
    private readonly IEventStorage _storage;
    private static object _lockStorage = new object();

    public Repository(IEventStorage storage)
    {
        _storage = storage;
    } 

    public void Save(AggregateRoot aggregate, int expectedVersion)
    {
        if (aggregate.GetUncommittedChanges().Any())
        {
            lock (_lockStorage)
            {
                var item = new T();

                if (expectedVersion != -1)
                {
                    item = GetById(aggregate.Id);
                    if (item.Version != expectedVersion)
                    {
                        throw new ConcurrencyException(string.Format("Aggregate {0} has been previously modified",
                                                                        item.Id));
                    }
                }

                _storage.Save(aggregate);
            }
        }
    }

    public T GetById(Guid id)
    {
        IEnumerable<Event> events;
        var memento = _storage.GetMemento<BaseMemento>(id);
        if (memento != null)
        {
            events = _storage.GetEvents(id).Where(e=>e.Version>=memento.Version);
        }
        else
        {
            events = _storage.GetEvents(id);
        }
        var obj = new T();
        if(memento!=null)
            ((IOriginator)obj).SetMemento(memento);
            
        obj.LoadsFromHistory(events);
        return obj;
    }
}

这个方法主要是用来对事件进行持久化的。 所有的聚合的变动都会存在该Repository中,首先,检查当前的聚合是否和之前存储在storage中的聚合一致,如果不一致,则表示对象在其他地方被更改过,抛出ConcurrencyException,否则将该变动保存在Event Storage中。

IEventStorage用来存储所有的事件,其实现类型为InMemoryEventStorage。

public class InMemoryEventStorage:IEventStorage
{
    private List<Event> _events;
    private List<BaseMemento> _mementos;

    private readonly IEventBus _eventBus;

    public InMemoryEventStorage(IEventBus eventBus)
    {
        _events = new List<Event>();
        _mementos = new List<BaseMemento>();
        _eventBus = eventBus;
    }

    public IEnumerable<Event> GetEvents(Guid aggregateId)
    {
        var events = _events.Where(p => p.AggregateId == aggregateId).Select(p => p);
        if (events.Count() == 0)
        {
            throw new AggregateNotFoundException(string.Format("Aggregate with Id: {0} was not found", aggregateId));
        }
        return events;
    }

    public void Save(AggregateRoot aggregate)
    {
        var uncommittedChanges = aggregate.GetUncommittedChanges();
        var version = aggregate.Version;
            
        foreach (var @event in uncommittedChanges)
        {
            version++;
            if (version > 2)
            {
                if (version % 3 == 0)
                {
                    var originator = (IOriginator)aggregate;
                    var memento = originator.GetMemento();
                    memento.Version = version;
                    SaveMemento(memento);
                }
            }
            @event.Version=version;
            _events.Add(@event);
        }
        foreach (var @event in uncommittedChanges)
        {
            var desEvent = Converter.ChangeTo(@event, @event.GetType());
            _eventBus.Publish(desEvent);
        }
    }

    public T GetMemento<T>(Guid aggregateId) where T : BaseMemento
    {
        var memento = _mementos.Where(m => m.Id == aggregateId).Select(m=>m).LastOrDefault();
        if (memento != null)
            return (T) memento;
        return null;
    }

    public void SaveMemento(BaseMemento memento)
    {
        _mementos.Add(memento);
    }
}

在GetEvent方法中,会找到所有的聚合根Id相关的事件。在Save方法中,将所有的事件保存在内存中,然后每隔三个事件建立一个快照。可以看到这里面使用了备忘录模式。

然后在foreach循环中,对于所有的没有提交的变更,EventBus将该事件发布出去。

现在,所有的发生变更的事件已经记录下来了。事件已经被发布到EventBus上,然后对应的EventHandler再处理对应的事件,然后与DB交互。现在来看EventBus的Publish方法。

public class EventBus:IEventBus
{
    private IEventHandlerFactory _eventHandlerFactory;

    public EventBus(IEventHandlerFactory eventHandlerFactory)
    {
        _eventHandlerFactory = eventHandlerFactory;
    }
        
    public void Publish<T>(T @event) where T : Event
    {
        var handlers = _eventHandlerFactory.GetHandlers<T>();
        foreach (var eventHandler in handlers)
        {
            eventHandler.Handle(@event);
        }
    }
}

可以看到EventBus的Publish和CommandBus中的Send方法很相似,都是首先通过EventHandlerFactory查找对应Event的Handler,然后调用其Handler方法。比如

public class StructureMapEventHandlerFactory : IEventHandlerFactory
{
    public IEnumerable<IEventHandler<T>> GetHandlers<T>() where T : Event
    {
        var handlers = GetHandlerType<T>();
            
        var lstHandlers = handlers.Select(handler => (IEventHandler<T>) ObjectFactory.GetInstance(handler)).ToList();
        return lstHandlers;
    }

    private static IEnumerable<Type> GetHandlerType<T>() where T : Event
    {
           
        var handlers = typeof(IEventHandler<>).Assembly.GetExportedTypes()
            .Where(x => x.GetInterfaces()
                .Any(a => a.IsGenericType && a.GetGenericTypeDefinition() == typeof(IEventHandler<>)))
                .Where(h => h.GetInterfaces()
                    .Any(ii => ii.GetGenericArguments()
                        .Any(aa => aa == typeof(T))))
                 .ToList();
        return handlers;
    }
}

然后返回并实例化了ItemCreatedEventHandler 对象,该对象的实现如下:

public class ItemCreatedEventHandler : IEventHandler<ItemCreatedEvent>
{
    private readonly IReportDatabase _reportDatabase;
    public ItemCreatedEventHandler(IReportDatabase reportDatabase)
    {
        _reportDatabase = reportDatabase;
    }
    public void Handle(ItemCreatedEvent handle)
    {
        DiaryItemDto item = new DiaryItemDto()
            {
                Id = handle.AggregateId,
                Description =  handle.Description,
                From = handle.From,
                Title = handle.Title,
                To=handle.To,
                Version =  handle.Version
            };

        _reportDatabase.Add(item);
    }
}

可以看到在Handler方法中,从事件中获取参数,然后新建DTO对象,然后将该对象更新到DB中。

到此,整个Command执行完成。

六 结语

CQRS是一种思想很简单清晰的设计模式,他通过在业务上分离操作和查询来使得系统具有更好的可扩展性及性能,使得能够对系统的不同部分进行扩展和优化。在CQRS中,所有的涉及到对DB的操作都是通过发送Command,然后特定的Command触发对应事件来完成操作,这个过程是异步的,并且所有涉及到对系统的变更行为都包含在具体的事件中,结合Eventing Source模式,可以记录下所有的事件,而不是以往的某一点的数据信息,这些信息可以作为系统的操作日志,可以来对系统进行回退或者重放。

CQRS 模式在实现上有些复杂,很多地方比如AggregationRoot、Domain Object都涉及到DDD中的相关概念,本人对DDD不太懂。这里仅为了演示CQRS模式,所以使用的例子是codeproject上的,末尾列出了一些参考文章,如果您想了解更多,可以有针对性的阅读。

最后,希望CQRS模式能让您在设计高性能,可扩展性的程序时能够多一种选择和考虑。

七 参考文献

  1. CQRS入門  http://www.codeproject.com/Articles/555855/Introduction-to-CQRS
  2. CQRS  http://martinfowler.com/bliki/CQRS.html
  3. CQRSジャーニー  http://msdn.microsoft.com/en-us/library/jj554200.aspx
  4. コマンドとクエリの責任分掌(CQRS)パターン  http://msdn.microsoft.com/en-us/library/dn568103.aspx
  5. ドメイン駆動設計の練習EntityFramework:CQRS建築パターン  http://www.cnblogs.com/daxnet/archive/2010/08/02/1790299.html
  6. イベントソーシングパターン  http://msdn.microsoft.com/en-us/library/dn589792.aspx

おすすめ

転載: www.cnblogs.com/youring2/p/10990984.html