隔离,通过使用不同的接口,从操作读取数据更新数据的操作。这种模式可以最大限度地提高性能,可扩展性和安全性;支持系统在通过较高的灵活性,时间的演变;防止更新命令,从造成合并在域级别上的冲突。
在传统的数据管理系统中,这两个命令(更新数据)和查询(请求数据),针对在一个单一的数据存储库中的相同的一组实体的执行。这些实体可以是在关系数据库中的一个或多个表,如 SQL Server 的行的子集。
典型地,在这些系统中,所有的创建,读取,更新和删除(CRUD)操作被施加到该实体的相同的表示。例如,一个数据传输对象(DTO)的代表顾客从数据存储中检索由数据访问层(DAL)并显示在屏幕上。用户更新 DTO 的某些领域(也许是通过数据绑定)和 DTO,然后保存回数据存储在 DAL。相同的 DTO 同时用于读取和写入操作,如图1所示。
图1 - 一个传统的 CRUD 架构
传统的 CRUD 设计工作良好时,只有施加到数据操作有限的业务逻辑。由开发工具提供可以非常快速地创建数据访问代码的支架机构,根据需要,可再进行定制。
然而,传统的 CRUD 方法有一些缺点:
注意: 对于的 CRUD 方法的局限性有了更深的了解请参见“CRUD,只有当你能负担得起”MSDN 上。
命令和查询职责分离(CQRS)是偏析,通过使用独立的接口读取操作的更新数据(命令)的数据(查询)的操作模式。这意味着,用于查询和更新的数据模型是不同的。该模型可随后被分离,如在图 2 中,虽然这不是绝对的要求。
图2 - 一个基本的 CQRS 架构
相比于数据(从该开发商建立自己的概念模式)的单个模型中固有的 CRUD 为基础的系统中,使用单独的查询和更新模型中 CQRS 为基础的系统中的数据显着地简化设计和实施。然而,一个缺点是,不像 CRUD 的设计,CQRS 代码不能自动用支架的机制产生。
查询模型读取数据和写入数据可以访问相同的实体店,也许是通过使用 SQL 视图的更新模型,或产生对飞预测。但是,它是常见的数据分成不同的物理存储来提高性能,可扩展性和安全性;如图3。
图3 - 一个 CQRS 架构,具有独立读写店
所读取的存储可以是只读副本写入存储区,或读取和写入存储可以具有不同的结构完全。使用 read 店的多个只读副本可以大大提高查询性能和应用程序的UI响应速度,尤其是在分布式场景下的只读副本靠近应用程序实例。一些数据库系统,如 SQL Server,提供额外的功能,如故障转移副本,以最大限度地提高可用性。
读的分离和写入存储还允许每个到会适当缩放以匹配负载。例如,读取存储通常会遇到一个更高的负载写入存储。
当查询/读取模型中包含的非规范化的信息(见物化视图模式),性能正在读取数据的每一个视图时在应用程序中或在查询系统中的数据时最大化。
有关CQRS模式及其实现的详细信息,请参阅以下资源:
在决定如何实现这个模式时,请考虑以下几点:
对于最终一致性的说明,请参阅数据一致性底漆。
这种模式非常适合于:
这种模式可能不适合于下列情况:
CQRS 模式常用于与事件获取图案一起使用。 CQRS 为基础的系统使用分离的读取和写入的数据模型,每个针对有关任务和通常位于物理上分离的存储区。当与采购活动时,事件的存储是写模式,这是信息的权威来源。一个 CQRS 为基础的系统的读取模型提供数据的物化视图,通常是高度非规范化的意见。这些视图量身定做的接口和应用程序,这有助于最大程度地显示和查询性能的显示要求。
使用事件作为写入存储区,而不是实际的数据的流,在一个时间点,避免了在单个聚合更新冲突并最大限度地提高性能和可扩展性。该事件可用于异步生成用于填充读取存储器中的数据的实体化视图。
由于事件存储是信息的权威来源,就可以删除物化视图和回放所有过去的事件来创建当前状态的一个新表示当系统升级时,或者当读取模式必须改变。物化视图是有效的数据的耐用只读缓存。
当使用 CQRS 结合事件获取模式,考虑以下几点:
注意: 欲了解更多信息,请参阅活动采购模式和物化视图模式,以及模式与实践指导 CQRS 之旅 MSDN 上。尤其是你应该阅读的章节介绍采购活动进行全面的探索模式,以及它如何与 CQRS 有用的,而章 CQRS 和 ES 深潜了解更多,包括如何聚集分区可以在微软的 Azure CQRS 使用。
下面的代码显示了一个 CQRS 实现,它使用不同的定义读取和写入模型为例某些提取物。该模型的接口没有规定的基础数据存储的任何功能,并且可以发展和进行微调独立,因为这些接口是分开的。
下面的代码演示了读取的模型定义。
// Query interface
namespace ReadModel
{
public interface ProductsDao
{
ProductDisplay FindById(int productId);
IEnumerable<ProductDisplay> FindByName(string name);
IEnumerable<ProductInventory> FindOutOfStockProducts();
IEnumerable<ProductDisplay> FindRelatedProducts(int productId);
}
public class ProductDisplay
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal UnitPrice { get; set; }
public bool IsOutOfStock { get; set; }
public double UserRating { get; set; }
}
public class ProductInventory
{
public int ID { get; set; }
public string Name { get; set; }
public int CurrentStock { get; set; }
}
}
该系统允许用户率的产品。应用程序代码通过使用在下面的代码中所示的 RateProduct 命令执行此操作。
<span></span><pre class="csharp" name="code">public interface Icommand
{
Guid Id { get; }
}
public class RateProduct : Icommand
{
public RateProduct()
{
this.Id = Guid.NewGuid();
}
public Guid Id { get; set; }
public int ProductId { get; set; }
public int rating { get; set; }
public int UserId {get; set; }
}
本系统采用 ProductsCommandHandler 类来处理由应用程序发出的命令。客户端通常通过消息传送系统发送命令到域,如一个队列。命令处理程序接受这些命令,并调用域接口的方法。每个命令的粒度被设计成减轻冲突请求的机会。下面的代码显示了 ProductsCommandHandler 类的轮廓。
public class ProductsCommandHandler :
ICommandHandler<AddNewProduct>,
ICommandHandler<RateProduct>,
ICommandHandler<AddToInventory>,
ICommandHandler<ConfirmItemShipped>,
ICommandHandler<UpdateStockFromInventoryRecount>
{
private readonly IRepository<Product> repository;
public ProductsCommandHandler (IRepository<Product> repository)
{
this.repository = repository;
}
void Handle (AddNewProduct command)
{
...
}
void Handle (RateProduct command)
{
var product = repository.Find(command.ProductId);
if (product != null)
{
product.RateProuct(command.UserId, command.rating);
repository.Save(product);
}
}
void Handle (AddToInventory command)
{
...
}
void Handle (ConfirmItemsShipped command)
{
...
}
void Handle (UpdateStockFromInventoryRecount command)
{
...
}
}
下面的代码显示了写模式 ProductsDoman 接口。
public interface ProductsDomain
{
void AddNewProduct(int id, string name, string description, decimal price);
void RateProduct(int userId int rating);
void AddToInventory(int productId, int quantity);
void ConfirmItemsShipped(int productId, int quantity);
void UpdateStockFromInventoryRecount(int productId, int updatedQuantity);
}
还要注意如何 ProductsDomain 接口包含在域中的意义的方法。通常情况下,在一个 CRUD 环境中,这些方法将有通用名称,如保存或更新,并有一个 DTO 作为唯一的参数。该 CQRS 方法可以更好地定制,以满足该组织开展业务及库存管理的方式。