【问题标题】:Why is GetByID a command rather than a query?为什么 GetByID 是命令而不是查询?
【发布时间】:2017-09-19 08:54:52
【问题描述】:

请参阅以下文章: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 描述为命令,即在这两种情况下,方法返回域对象(客户)而不是 DTO 对象(客户DTO)。为什么是这样? GetByID 从数据库返回数据。它应该是一个查询,即返回 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?两者都有?

【问题讨论】:

    标签: c# domain-driven-design cqrs


    【解决方案1】:

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

    更新后:

    这是正确的:

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

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

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

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

    【讨论】:

      【解决方案2】:

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

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

      【讨论】:

        【解决方案3】:

        从第一篇文章开始:

        命令端

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

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

        【讨论】:

        • 它在 CommandRepository 中。这不是命令吗?
        • 没有。它在这里用于从 "write side" 检索您的聚合,您将在其上应用您的命令。
        • 谢谢。使用 CQRS 是否必须在同一个有界上下文中拥有读取存储库和写入存储库?
        • 因为两者都作用于不同的模型聚合与 DTO。他们应该分开。最后,写入和查询端可以被视为不同的有界上下文。他们都在处理一些重叠的数据,但他们的建模方式不同。
        【解决方案4】:

        一般(和 CQS):

        • 命令不返回任何内容并且有副作用。
        • 查询返回有用的信息,没有副作用并且是幂等的。

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

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

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

        【讨论】:

        • 我称它为命令是因为该方法查询/读取数据而不是写入(命令)数据。这不正确吗?
        • 我描述了 CQS 中命令和查询的含义。 CQRS 是它的一个更“分布式”的版本。但这并没有改变术语的本质。 martinfowler.com/bliki/CommandQuerySeparation.html
        • 感谢您提及副作用。 +1。你能看看我的 uodated posr 吗?
        【解决方案5】:

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

        一个查询。

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

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

        查询通常理解为 Bertrand Meyer 在描述 Command Query Separation 时所表达的

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

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

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

        ...但我们通常不会。 CQRS 是从 Distributed Domain Driven Design 演变而来的,正如您可能猜到的那样,它深受 [tag:domaindriven design] 的影响。 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 数据库(毕竟,写入持久存储的地方),并且存储库可能需要在开始计算之前刷新其状态的本地副本它会写。

        【讨论】:

        • 感谢您对 DTO 的引用。 +1。你能看看我更新的帖子吗?
        • 我认为编辑后的帖子转移到了另一个关注点。对于您是否需要将聚合或将持久状态映射到它的问题,我会说 - 都不需要。我更喜欢将聚合作为静态状态机并将聚合状态分开,因此我不需要映射它并将持久性问题与业务问题分开。此外,对于第一个示例代码,我不喜欢在聚合存储库中使用 Search。查询是针对 CQRS 的读取端,Search 绝对是查询。
        • @Alexey Zimarev,GetByID 从数据库中读取,但它包含在写入存储库中。你指的不同的问题是什么?
        猜你喜欢
        • 2023-03-21
        • 1970-01-01
        • 2010-10-10
        • 1970-01-01
        • 1970-01-01
        • 2015-10-03
        • 2012-06-23
        • 2012-05-11
        • 1970-01-01
        相关资源
        最近更新 更多