2017 年 2 月
第 32 卷,第 2 期
领先技术 - 利用事件和 CQRS 实现内部商业智能
作者 Dino Esposito | 2017 年 2 月
为了确认业务流程的有效性和团队正在打造的用户体验的整体质量,我最近一直在与一位客户讨论一些线框。系统中有一些共享的工作项,用户可以选择并开始处理这些项。与我讨论的客户是一位全能型技术人员,但她所能想到的只是一个关系数据库表,其中记录了谁选择了什么以及当前的处理状态。
不用说,类似的体系结构当然可以用。但使用基于事件的不同体系结构,可以捕捉更多有关系统事件的信息。因此,核心问题就在于: 客户现在或在不久的将来是否需要访问和使用从事件中获得的额外信息?
在本期专栏中,我将紧接着上个月那一期的专栏,向处理会议室预订的示例应用程序添加商业智能 (BI) 模块。BI 一词是最近出现频率相当高的流行语,通常与特定的技术和产品相关联。然而,BI 的核心是指一系列技术,用于捕捉原始数据并将其转换成分析员可使用的实用信息,方便其改进流程或仅用作统计数据。
通过向应用程序添加事件溯源,可以获取原始数据。将事件溯源和 CQRS 相结合,最终可以将原始业务事件存储在命令堆栈中,这些事件经过非规范化正确处理,以便实现应用程序核心功能。不过,你随时都可以添加额外模块,用于读取原始数据并将其转换成其他有意义的信息块,以满足可能的任何业务目的。
绝不会错过任何事情
突出介绍事件溯源的优势时,提到的第一点通常是: “使用事件,绝不会错过系统内发生的任何事情。” 这种说法再真实不过,但或许还需要更实事求是地阐明原因。请看图 1。
图1:可在原始事件的基础上生成多个数据投影
在创建、读取、更新、删除 (CRUD) 系统中,通常有一个数据(大多是关系数据)表示层,以及一个或多个简单的数据投影。大多数情况下,这些投影只用于调整表格数据,以满足表示层的需求。使用事件溯源,可以进一步深化此模型的使用,关键因素在于降低已存储数据的抽象层级。存储的域准确信息越多,今后可随时生成的投影就越多越丰富。
在软件系统中,用户操作是最小单位的可观察行为,由这些操作生成的业务事件是可存储的最基本信息。在过去一期专栏 (msdn.com/magazine/mt790196) 中,我通过 NuGet 使用 MementoFX 框架,透明地处理和保留域聚合对象生存期内的相关事件。我还使用了特殊的同步工具(称为“非规范化器”),在每个相关业务事件发生后创建可见的投影。专栏最后展示了如何根据“事件-命令-Saga (ECS)”模式重写 CRUD 系统。现在,我们来了解一下如何添加另一个投影来填充面向管理员的仪表板屏幕。
生成你自己的 BI 层
图 2 展示了示例应用程序的主屏幕。正如之前提到的,这是一个共享资源(如一组会议室)的预订系统。
图 2:示例预订系统的 UI
从图中可以看出,能够登录系统的所有用户都可以预订会议室,并能更改或取消预订。面向 CRUD 的经典系统中有一个预订表,其中每个记录表示一个预订。在用户更改预订后,开始时间和时长遭到覆盖;在用户取消预订后,记录直接遭到删除。对记录进行普通查询始终返回预订的当前状态。如果将普通 CRUD 转换成历史 CRUD(请参阅本专栏的 2016 年 5 月和 6 月刊,网址分别为 msdn.com/magazine/mt703431 和 msdn.com/magazine/mt707524),可以跟踪固定数量的事件,并将它们与任何相关信息一起保存在其他一些表中。那么,基于 MementoFX 的事件溯源解决方案在什么方面优于简单的历史 CRUD 呢? 优势全都集中在最终软件项目的代码灵活性和可复原性上。
使用 MementoFX,可以关注相关的域行为和事件。根据这些需求对域对象进行建模后就可以放手不管了。可以生成一个 API,用于查询发生的事情、系统在给定日期和时间的状态,以及是记录了所有事件,还是为了实现某种临时业务用途只以某种自定义方式聚合和转换了原始数据。这就是所谓的 BI 的本来意义和本质。
生成事件的纯日志
就示例应用程序而言,有一个存储所有原始事件的 RavenDB 数据库,还有一个包含待处理预订列表的 SQL Server 非规范化表。存储的事件包括预订创建事件以及改为预订其他时间的所有后续事件,甚至包括预订取消事件。由于预订取消事件不再显示在主 UI 中,因此中间位置显示的是预订更改事件。虽然所有这些事件与生成主 UI 都不相关,但却对生成面向管理员的仪表板 UI 至关重要。
例如,我们来了解一下如何对系统中的所有预订(或在给定时间间隔创建的预订)进行分组,并显示每个预订的全部历史记录,如图 3 所示。
图 3:系统内事件的完整日志
虽然系统统计的待处理预订为 16 个,但每个预订还包括一个或多个事件。例如,突出显示的预订是在创建后两次更改为不同日期的不同时段。
若要生成类似屏幕,必须明确查询事件存储中的所有事件,然后对其进行处理,生成符合预期 UI 要求的框架。生成实际视图的 Razor 代码收到数据模型,如下所示:
public class BookingWithHistory
{
public BookingWithHistory()
{
History = new List<BookingHistory>();
}
public BookingSummary Current { get; set; }
public IList<BookingHistory> History { get; set; }
}
BookingSummary 类表示给定预订的当前状态,并且是图 1 主视图的幕后类。
public class BookingSummary : Dto
{
public Guid BookingId { get; set; }
public string DisplayName { get; set; }
public DateTime Day { get; set; }
public int StartHour { get; set; }
public int StartMins { get; set; }
public int NumberOfSlots { get; set; }
public BookingReason Reason { get; set; }}
此类的实例在每个“创建”或“更改”操作发生后的非规范化过程中进行创建。此类通过实体框架保留和读取到经典 SQL Server 数据库中,反之亦然。换句话说,此类就是根据当前最新的状态快照形成默认系统视图的项。从技术角度来讲,此类属于读取模型。
图 3 中的视图还从事件存储(在此示例中,为 RavenDB 存储)中记录的原始事件收集数据。以下代码片段展示了如何对事件存储运行查询,以获取在具有给定 ID 的预订的生存期内发生的所有相关事件:
var createdEvents = EventStore.Find<NewBookingCreatedEvent>(e =>
e.BookingId == bookingId).ToList();
var movedEvents = EventStore.Find<BookingMovedEvent>(e =>
e.BookingId == bookingId).ToList();
var deletedEvents = EventStore.Find<BookingCanceledEvent>(e =>
e.BookingId == bookingId).ToList();
合并这些事件就可以生成给定聚合的完整历史记录。重播所有这些事件(即按顺序将每个事件的状态应用于聚合对象的新实例)可以返回对象的当前状态,以用于显示或处理目的。不言而喻,可以将日期筛选器添加到事件查询中,从而重新生成域对象(即预订)在给定日期前的最新状态。
推断一些业务信息
假设你是负责内部流程的管理员。根据角色,你希望有效使用共享资源(如会议室)。该如何验证使用是否有效?又该如何相应地更新预订策略呢? 图 4 提供了有助于回答此类问题的有用信息。
饼图展示了在时间间隔内创建的预订数,以及稍后更改或取消的预订数。相反,条形图则展示了每周的预订创建、更改或取消数。经过粗略的初步分析,大概有近一半的预订在某个时间点发生了变更,大约有四分之一的预订被取消。
如果你是希望改进流程的管理员,图 4 中的图表则向你发出了警报。无论采用何种分析方式,都会发现预订后的更改数非常多。这是否会妨碍其他潜在用户轻松预订会议室? 若要消除此警报,不妨查看同一时间间隔内每个会议室的平均覆盖率。如果仪表板中还没有这一特定的覆盖率图表,请让开发者添加这张图表,这实际上并不需要很长的时间。最后,只需为 LINQ 样式查询编写另一个谓词即可:
var eventsByWeek = bookingStatuses.GroupBy(b => b.WeekDate).ToList();
foreach (var weekEvents in eventsByWeek)
{
var wr = new WeekActivityReport
{
StartOfWeek = weekEvents.Key,
TotalBookingCreated = weekEvents.Count(),
TotalBookingMoved = weekEvents.Count(b => b.Moved),
TotalBookingDeleted = weekEvents.Count(b => b.Deleted)
};
reports.Add(wr);
}
图 4:给定时间间隔内预订操作的图形报表
尤其是,示例应用程序获取给定时间间隔内所有预订的所有事件,并按周对这些事件进行分组。接下来,示例应用程序创建每周活动报表对象,然后将其轻松传递给 ChartJS,以生成令人印象深刻的图形。
我想在本期专栏中强调的事件溯源的最重要一方面是,只要在最低抽象层级保存了原始信息,就可以在不同的时期以多种不同的方式使用此类信息。再回到会议室演示,你可以在连续发布期间部署管理器仪表板,也可以再生成一个全新产品。还可以分析应用程序生存期内发生的所有事件,并以此为基础生成新的数据投影,以实现任何可能的业务目标。然而,最重要的是,大部分情况下,任何连续开发工作并不依赖于已生成的部分,因此也不会对其造成影响。这就是你为了结合使用 CQRS 和事件溯源而进行的任何投资的最终回报。
总结
在软件中处理业务事件屡见不鲜。可以使用历史 CRUD (H-CRUD) 达到类似效果。H-CRUD 只是任何一类精心制作的解决方案的别名,此类解决方案可方便你跟踪业务对象的所有不同状态。事件溯源也可达到同样效果,区别在于它在不同的抽象层级运行,并且在专业化体系结构的上下文(如 CQRS)中依赖于更强大的定制工具(如事件存储)和模式(如事件溯源)。
我在 MSDN 杂志中撰写有关 CQRS 和事件溯源的文章已有相当长的一段时间,与大多数人一样,我们都赞成 CQRS 和事件的相关性,但都难于确定从何入手。我建议这些人回顾一下我在 5 月刊 (msdn.com/magazine/mt703431) 和 7 月刊 (msdn.com/magazine/mt707524) 中撰写的有关 H-CRUD 的内容,然后回看近期更多有关 ECS 模式(亦称为“CQRS/ES”)和 MementoFX 的文章。这应该有助于他们从熟悉的思维方式入手,然后逐渐深入,直到可以更强大的全新方式来完成以前的工作。
这一切都表明,软件与魔法或宗教无关,而是关乎于如何处理各项事务,最好方便客户和开发团队操作。除事件存储、总线、事件模式和体系结构之外,MementoFX 也有助于快速有效地处理各项事务。
Dino Esposito是《Microsoft .NET: 构建面向企业的应用程序》(Microsoft Press,2014 年)和《使用 ASP.NET 构建新型 Web 应用程序》(Microsoft Press,2016 年)的作者。Esposito 是 JetBrains 公司 .NET 和 Android 平台的技术推广专家,经常在全球性行业活动上发表演讲,他在 software2cents.wordpress.com 和 Twitter: @despos.上分享了他的软件构想。
衷心感谢以下 Microsoft 技术专家对本文的审阅: Andrea Saltarello