CQRS + ES - 查询业务逻辑所需数据的位置

本文关键字:数据 位置 ES 查询 业务 CQRS | 更新日期: 2023-09-27 17:56:59

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

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

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

CQRS + ES - 查询业务逻辑所需数据的位置

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

使用文档db(mongodb,raven db或postgres)可能会使工作更容易。如果您遇到 rdbms 或键值存储的问题,请创建查询表,即写入模型的读取模型,充当索引:)(这假设您正在序列化对象)。如果您使用每种实体类型的特定表架构以关系方式存储内容(巨大的开销,使您的生活复杂化),则信息很容易自动查询。

为什么无法查询所涉及的聚合?

我冒昧地重写了目标:

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

在这里,我们有一个应该能够计算标准负载系数的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;
}

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

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

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

在事件源模型中,域存储库永远无法提供任何查询功能。Yves Reynhout的AggregateSource是一个很好的参考,这里是那里的IRepository接口。您可以轻松地看到此界面中没有任何"查询"方法。

CQRS 中还有一个类似的问题域查询