【问题标题】:CQRS + ES - Where to query Data needed for business logic?CQRS + ES - 在哪里查询业务逻辑所需的数据?
【发布时间】:2015-06-17 17:22:19
【问题描述】:

我正在使用 CQRS + ES,但我遇到了一个无法找到解决方案的建模问题。
您可以跳过以下内容并回答标题中的通用问题:您将在哪里查询业务逻辑所需的数据?
抱歉,原来这是一个复杂的问题,我的想法现在很扭曲!!!
问题出在:
我的用户是团队成员。这是多对多的关系。每个用户都有每个团队的可用性状态。
团队收到票,每张票都有一定的负载系数,应该根据他们的可用性和总负载分配给团队的一名成员。
第一个问题,我需要查询团队中可用的用户列表,并选择负载最少的用户,因为他有资格分配。(请注意,这是其中一种情况,它可能是运行不同的查询)
第二个问题,票的负载因子可能会改变,所以我在计算时必须考虑到这一点每个用户的总负载。请注意,虽然工单可以属于 1 个团队,但分配应基于用户的总负载,而不是每个团队的负载。
目前,此有界上下文接收到 TicketReceivedEvent,我应该触发工作流将该票分配给用户。
可能的解决方案:

  1. 最简单的方法是将事件排队并顺序发送命令 AssignTicketToUser 并让服务查询用户 ID 的读取模型,获取用户和 user.assignTicket(Ticket)。一旦收到 TicketAssignedEvent,就发送下一个分配命令。但是从命令处理程序中查询读取模型似乎是一个危险信号!并且很麻烦地排队购买所有这些门票!
  2. 为每个用户分配一个流程经理,并将其可用性/团队和分配给该用户的工单。在这种情况下,我们将查询替换为“进程管理器查找”查询,命令处理程序将调用 Ticket.AssignTo(User)。缺点是我认为太多业务逻辑泄漏到域模型之外,特别是我们从用户聚合中提取所有信息/模型以使其可用于查询

我倾向于使用第一个解决方案,它似乎更易于维护、修改/扩展和在代码中定位,但也许我缺少一些东西。

【问题讨论】:

    标签: c# cqrs event-sourcing


    【解决方案1】:

    始终(嗯,99.99% 的情况)在业务/域层,即在 CQRS 的“命令”部分。这意味着您的存储库应该具有用于​​特定查询的方法,并且您的持久性模型应该足够“可查询”以达到此目的。这意味着在决定如何实现持久性之前,您必须更多地了解您的域的用例。

    使用文档数据库(mongodb、raven db 或 postgres)可能会使工作更轻松。如果您坚持使用 rdbms 或键值存储,请创建查询表,即写入模型的读取模型,充当索引:)(这假设您正在序列化对象)。如果您要存储与每种实体类型的特定表架构相关的内容(巨大的开销,您的生活会变得复杂),那么信息很容易自动查询。

    【讨论】:

    • 感谢答案,目前我正在使用 mssql 数据库并在其中建模事件存储。我序列化事件并保存它们。只是为了确保我理解正确,你是说我应该从命令处理程序查询读取端。
    • 不,我是说,您应该有一些查询索引,用于根据业务标准识别业务实体。它仍然是“命令”模型的一部分
    • 这属于我认为的第二个选项。我可以在我的流程管理器中保存所需的条件,然后由路由器完成查询以找到适当的 saga/流程管理器来处理 TicketCreatedEvent。
    • ES 存储库应该从不由于 ES 本身的性质提供任何查询接口。它应该只有添加、获取和删除。针对读取模型执行查询,该模型通过存储库访问。
    • ES 是一个实现细节。业务存储库应始终仅提供业务需求所需的查询。对于写入模型,读取模型应该是未知的,并且并非所有持久性存储都可以轻松查询,这就是为什么在持久性级别需要一种持久性查询形式的原因。顺便说一句,存储库不仅有 CRUD 职责。
    【解决方案2】:

    为什么不能查询所涉及的聚合?

    我冒昧地重写了目标:

    将团队票分配给总负载最低的用户。

    这里有一个Ticket,它应该能够计算标准负载因子,一个Team,它知道它的用户,还有一个User,它知道它的总负载并可以接受新票:

    更新:如果将存储库传递给聚合不合适,可以将其包装在服务中,在本例中为定位器。这样做可以更轻松地强制一次只更新一个聚合。

    public void AssignTicketToUser(int teamId, int ticketId)
    {
        var ticket = repository.Get<Ticket>(ticketId);
        var team = repository.Get<Team>(teamId);
        var users = new UserLocator(repository);
        var tickets = new TicketLocator(repository);
        var user = team.GetUserWithLowestLoad(users, tickets);
    
        user.AssignTicket(ticket);
    
        repository.Save(user);
    }
    

    我们的想法是 User 是我们更新的唯一聚合。

    Team 会知道它的用户:

    public User GetGetUserWithLowestLoad(ILocateUsers users, ILocateTickets tickets)
    {
        User lowest = null;
    
        foreach(var id in userIds)
        {
            var user = users.GetById(id);
            if(user.IsLoadedLowerThan(lowest, tickets))
            {
                lowest = user;
            }
        }
        return lowest;
    }
    

    更新:由于工单可能会随时间改变负载,User 需要计算其当前负载。

    public bool IsLoadedLowerThan(User other, ILocateTickets tickets)
    {
        var load = CalculateLoad(tickets);
        var otherLoad = other.CalculateLoad(tickets);
    
        return load < otherLoad;
    }
    
    public int CalculateLoad(ILocateTickets tickets)
    {
        return assignedTicketIds
            .Select(id => tickets.GetById(id))
            .Sum(ticket.CalculateLoad());
    }
    

    User 然后接受票证:

    public void AssignTicket(Ticket ticket)
    {
        if(ticketIds.Contains(ticket.Id)) return;
    
        Publish(new TicketAssignedToUser
            {
                UserId = id,
                Ticket = new TicketLoad
                    {
                        Id = ticket.Id,
                        Load = ticket.CalculateLoad() 
                    }
            });
    }
    
    public void When(TicketAssignedToUser e)
    {
        ticketIds.Add(e.Ticket.Id);
        totalLoad += e.Ticket.Load;
    }
    

    我会使用流程管理器/传奇来更新任何其他聚合。

    【讨论】:

    • 问题是工单负载可能会根据其状态或类别而改变。这意味着每次用户想要更新 Tixket 时,我都必须更新 2 个违反 DDD 原则/指南的聚合
    • 但是用户不能加载其分配的票证并计算实际负载吗?我的猜测是票务负载在不同的时间点发生变化。
    • 这是一个解决方案,但这是我尚未做出的决定:聚合是否可以访问存储库?
    • 我认为任何模式都不应该限制我们做正确的事情,或者鼓励我们做相反的事情。只要它作为依赖项传递并且没有更新,我认为访问存储库的聚合没有任何错误。当然,您可以将存储库包装在服务中。这样就没有要保存到的存储库,并且更容易强制执行不更新。
    【解决方案3】:

    您可以在应用服务中查询您需要的数据。这似乎与您的第一个解决方案相似。

    通常,您会交叉引用您的聚合,所以我不太确定第一个问题来自哪里。每个用户都应该有一个它所属的团队列表,每个组都有一个用户列表。您可以使用所需的任何属性来补充此数据,例如可用性。因此,当您阅读聚合时,您可以直接获得数据。当然,你会有很多数据重复,但这很常见。

    在事件溯源模型中,域存储库永远无法提供任何查询能力。 Yves Reynhout 的 AggregateSource 是一个很好的参考,这里是 IRepository interface。您可以很容易地看到这个界面中没有任何“查询”方法。

    还有一个类似的问题Domain queries in CQRS

    【讨论】:

    • -1 你把概念弄得一团糟。命令处理程序不应访问读取模型,因为读取模型不是它所关心的,并且读取模型可以生成 async 因此处理程序将使用不一致的模型。聚合根不查询任何东西,存储库会。 ES 意味着将对象状态表示为事件流,它是一个实现细节,它在定义存储库的接口时没有任何说法。最后,您将 UI/报告查询与域查询混淆了,这是另一回事。
    • 我没有写那个聚合查询读取模型。既然您如此确定,请在github.com/gregoryyoung/m-r/tree/master/SimpleCQRS 向我展示一个带有查询的存储库。问题是关于 CQRS + ES。如果这直接出现在所问的问题中,为什么我们现在要从 实现细节 中抽象出来???在事件源写入模型中,不可能进行任何查询,这是一个明确的声明。
    • 我同意从命令处理程序查询。这是我的错误,我相应地更改了答案。但我非常感谢 Greg Young 或 Eric Evans 对术语“域查询”的任何引用,在此先感谢您。
    • 你意识到你指的是一个简单的例子,它只需要“获取”吗?所有领域和要求都相同吗?不。对于许多开发人员 ES 和 CQRS 一起使用,这是一个 CQRS 问题,也是一个设计问题。存储库接口从不关心您使用的是 ES 还是其他任何东西。实现(域)用例的服务使用一个或多个 DDD 存储库。在某些情况下,您需要根据存储库中的标准获取所有相关聚合。那是您的域查询。随意想出一个更好的术语。 Greg Y. 或 Eric E. 批准的一项。
    • 迈克,我已经阅读了您在另一个 SO 问题中提到的帖子。我完全同意关于域与数据模型分离的说法,以及许多与 EF 相关的 DDD 教程将它们紧密地绑定在一起,这并不好。但是,您的许多陈述是一个很长的讨论主题,并且没有黑白之分。存储库模式是最主观的话题之一。您的愿景更符合 Fowler 的与 DDD 无关的存储库。这是我在这次讨论中的最后一篇文章。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-26
    • 2011-06-01
    • 1970-01-01
    • 2011-08-02
    • 2011-08-02
    • 1970-01-01
    相关资源
    最近更新 更多