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

Why is GetByID a command rather than a query?

请参阅以下文章:https://www.codeproject.com/Articles/555855/Introduction-to-CQRS and http://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) 都是 return 由方法而不是 DTO 对象 (CustomerDTO) 编辑的。为什么是这样? GetByID returns 数据库中的数据。它应该是一个查询,即 return CustomerDTO,不是吗?

更新 25/09/17

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

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

或者这个:

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

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

哪个片段支持CQRS?两个?

在这种情况下,我认为他们正在区分 returns 只读数据的查询,一旦到达 UI 就永远不会使用,和客户域对象,(可能)为了支持编辑而被获取,因此有一些 'higher' 目的。

在某些情况下,您的读取存储最终是一致的,您可能希望从您的主存储中获取单个客户对象,这是真实记录,而您可能 运行 您的查询您的阅读商店可能没有赶上所有已发布的更改。

来自第一篇文章:

Command side

Since the read side has been separated the domain is only focused on processing of commands. Now the domain objects no longer need to expose the internal state. Repositories have only a few query methods aside from GetById.

它并没有告诉你 GetById 是一个命令。而是 GetById 是存储库上的一种方法(用于检索可以在其上应用命令的聚合),它是命令堆栈的一部分。但它与查询堆栈中的查询无关。就这样。我没有全部看完,但我相信两篇文章的构思是一样的。

一般(和 CQS):

  • 命令 return 什么都没有,而且有副作用。
  • 查询return有用的东西,没有副作用,而且是幂等的。

这就是命令与查询的不同之处。我不明白你为什么将 GetByID 方法作为命令调用。

就 CQRS 而言,查询确实意味着 return 来自读取模型的数据。但是,查询没有包含在存储库中。 GetById 存储库中的方法需要在写入端获取域对象。然后,它由命令处理程序进行操作,并将更改保存在写入端。

这些操作与 CQRS 的 query/read 端无关。

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

更新后:

这是正确的:

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

The first fragment of code prevents hits on the write database, (I thought CQRS was suppose to prevent read hits on the write database)? However, GetByID also hits the write database.

它防止在读取模型上写入并在写入模型上读取。 但是,您可以从写入持久性加载写入模型(聚合根)以便向其发送命令 - 这就是 GetProduct(id).

的目的

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

Why is GetByID a command rather than a query?

一个查询。

They both describe GetByID as commands i.e. in both cases domain objects (Customer) are returned by the methods instead of DTO objects (CustomerDTO). Why is this? GetByID returns data from the database. It should be a query i.e. return CustomerDTO, shouldn't it?

数据 returned 的 shape 不会改变该方法是查询的事实。

查询通常被理解为 Bertrand Meyer 在描述 Command Query Separation

时所表达的

Queries: Return a result and do not change the observable state of the system (are free of side effects).

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

CQRS对command和query的理解是一样的,并且划分了职责。不改变系统可观察状态的用例由 "read model" 处理,试图修改系统可观察状态的用例由 "write model".

处理

如果我们对这个拆分进行伪编码,结果将完全符合您的预期

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) { /* … */ }
    }
}

...但我们通常不这样做。 CQRS evolved from Distributed Domain Driven Design, which as you might guess was heavily influenced by [tag:domain driven design]. DDD is strongly influenced by the object oriented style; the responsibility for modifying the state of the model should reside within the domain model (Tell, Don't Ask)。

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

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

应用核心逻辑不变

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

区别仅在于代码的组织方式。

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

这是您通常会在 CQRS 中看到的样式,因为它是您通常会在领域驱动设计中看到的样式:应用程序代码对底层数据的管理方式一无所知——它只讨论到域模型(存储库和产品)支持的接口。 Evans 使用分层架构 - 应用层与领域层对话,领域层与数据库对话。

I thought CQRS was suppose to prevent read hits on the write database

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 数据库(毕竟这是持久存储写入的地方),并且存储库可能需要刷新其本地状态副本在开始计算要写入的内容之前。