为什么GetByID是命令而不是查询?

时间:2017-09-19 08:54:53

标签: c# domain-driven-design cqrs

请参阅以下文章:https://www.codeproject.com/Articles/555855/Introduction-to-CQRShttp://enterprisecraftsmanship.com/2015/04/20/types-of-cqrs/。这是一些代码:

public class CustomerRepository
{
    public void Save(Customer customer) { /* … */ }
    public Customer GetById(int id) { /* … */ }
    public IReadOnlyList<CustomerDto> Search(string name) { /* … */ }
}

它们都将GetByID描述为命令,即在两种情况下,域对象(Customer)都由方法而不是DTO对象(CustomerDTO)返回。为什么是这样? GetByID从数据库返回数据。它应该是一个查询,即返回CustomerDTO,不应该吗?

更新25/09/17

假设我从数据库中检索产品。然后,我想在Product上运行一些方法(更改实例变量),然后将更改保存回数据库。我会这样做:

ProductDTO productDTO = ProductRepository.GetProduct(1);
DomainProduct domainProduct = AutoMapper.Mapper.Map<DomainProduct>(DomainProduct);
domainProduct.RunSomeMethod();

或者这个:

DomainProduct domainProduct = ProductRepository.GetProduct(1);
domainProduct.RunSomeMethod();

代码的第一个片段可以防止写入数据库上的命中(我认为CQRS是为了防止写入数据库上的读取命中)?但是,GetByID也会命中写入数据库。

哪个片段支持CQRS?既?

5 个答案:

答案 0 :(得分:1)

在这种情况下,我认为他们正在区分一个返回只读数据的查询,一旦到达UI就永远不会被使用的查询,以及一个(可能)被提取的Customer域对象。支持编辑的目的有一些“更高”的目的。

在某些情况下,您的读取存储最终是一致的,您可能希望从主存储中获取单个Customer对象,这是真实的记录,而您将针对您的读取存储运行查询,而这可能不是已经赶上了所有已发布的变化。

答案 1 :(得分:1)

从第一篇文章:

  

指挥方

     

由于读取方已分离,因此只关注域   处理命令。现在域对象不再需要了   暴露内部状态。 存储库只有几个查询方法   除了 GetById

它没有告诉你GetById是一个命令。而是GetById是一个方法(用于检索您可以在其上应用命令的聚合)在存储库上,这是命令堆栈的一部分。但它与查询堆栈中的查询无关。就这样。我还没有读完所有内容,但我相信这两篇文章的概念都是一样的。

答案 2 :(得分:1)

一般(和CQS):

  • 命令不返回任何副作用。
  • 查询返回有用的东西,没有副作用,是幂等的。

这是命令与查询的不同之处。我不明白为什么你把GetByID方法称为命令。

就CQRS而言,查询确实意味着从读取模型返回数据。但是,查询不包含在存储库中。存储库中的GetById方法需要在写入端获取域对象。然后,它由命令处理程序操纵,并且更改将保留在写入端。

这些操作与CQRS的查询/读取方无关。

答案 3 :(得分:1)

但是你的标题不正确。GetById不是这两篇文章中的命令(无论如何它都不能是command,可能是命令处理程序或命令方法)。但是在命令端使用

更新后:

这是正确的:

DomainProduct domainProduct = ProductRepository.GetProduct(1);
domainProduct.RunSomeMethod();
  

代码的第一个片段可以防止写入数据库上的命中(I   认为CQRS是为了防止写入数据库上的读取命中)?   但是,GetByID也会命中写入数据库。

它可以防止对读取模型进行写入并读取写入模型。 但是,您可以从写入持久性加载写入模型(聚合根)以向其发送命令 - 这是GetProduct(id)的目的。

在您的情况下,DomainProduct不应该有任何查询方法(即getter),只有命令方法(即activate())。这就是你如何阻止对写模型的读取:没有任何查询方法。 CQRS是CQS应用于所有模型上的任何方法。此限制仅对域实体应用 ;任何其他对象(即存储库)可以在写入端具有查询方法(如GetProduct(id))。

答案 4 :(得分:1)

  

为什么GetByID是命令而不是查询?

一个查询。

  

它们都将GetByID描述为命令,即在两种情况下,域对象(Customer)都由方法而不是DTO对象(CustomerDTO)返回。为什么是这样? GetByID从数据库返回数据。它应该是一个查询,即返回CustomerDTO,不应该吗?

返回的数据的形状不会改变该方法是查询的事实。

Bertrand Meyer在描述Command Query Separation

时通常理解查询
  

查询:返回结果,不要改变系统的可观察状态(没有副作用)。

在这种情况下,结果恰好是一个域对象而不是DTO,但它仍然是迈耶意义上的查询。

CQRS对命令和查询具有相同的理解,并将责任分开。不改变系统的可观察状态的用例由“读取模型”处理,并且用例试图通过“写入模型”修改系统的可观察状态。

如果我们伪分割这个分割,结果看起来就像你期望的那样

namespace ReadModel {
    public class CustomerRepository
    {
        public IReadOnlyList<CustomerDto> Search(string name) { /* … */ }
    }
}

我们可以在写模型中重复这种方法......

namespace WriteModel {
    public class CustomerRespository {
        public CustomerDto GetById(int id) { /* … */ }
        public void Save(CustomerDto customer) { /* … */ }
    }
}

......但我们通常不这样做。 CQRSDistributed Domain Driven Design演变而来,正如您可能猜到的那样,受到[tag:域驱动设计]的严重影响。 DDD受面向对象风格的强烈影响;修改模型状态的责任应该在域模型(Tell, Don't Ask)内。

因此,在写模型中,我们不从存储库返回状态,而是返回对域模型实体的引用,该实体更新其自身状态以响应命令

namespace WriteModel {
    public class CustomerRespository {
        public Customer GetById(int id) { /* … */ }
        public void Save(Customer customer) { /* … */ }
    }
}

应用程序的核心逻辑没有改变

  • 从记录簿中读取旧状态
  • 使用旧状态计算新状态
  • 将旧状态替换为记录簿中的新状态

区别在于代码的组织。

DomainProduct domainProduct = ProductRepository.GetProduct(1);
domainProduct.RunSomeMethod();

这是您在CQRS中通常会看到的样式,因为它是您在域驱动设计中通常会看到的样式:应用程序代码不知道如何管理基础数据 - 它只与域模型(存储库和产品)支持的接口。 Evans使用分层架构 - 应用层与域层进行通信,域层与数据库进行通信。

  

我认为CQRS是为了防止写入数据库上的读取命中

CQRS中的关键思想是您在阅读时使用的对象不是您在编写时使用的对象。

// I'm in a write use case
Product product = productRepository.getProduct(1);
product.changeTheProductState(...);

// I'm in a read use case
ProductView view = productRepository.getView(1);
return view.queryCurrentState();

如果我想要做的就是询问有关产品状态的问题,那么我会从存储库中获取一个连接到 read 数据库的对象,并提出要求。如果读取的次数比写入次数高10倍,那么就可以防止对写入数据库的大量命中。

但是写入仍然连接到 write 数据库(毕竟这是写入持久存储的地方),并且存储库可能需要在开始计算之前刷新其状态的本地副本它会写。