CQRS + ES - 在哪里查询业务逻辑所需的数据?

时间:2015-06-17 17:22:20

标签: c# cqrs event-sourcing

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

  1. 最简单的方法是对事件进行排队并依次发送命令AssignTicketToUser,并让服务查询用户ID的读取模型,获取用户和user.assignTicket(Ticket)。收到TicketAssignedEvent后,发送下一个赋值命令。但是从命令处理程序中查询读取模型似乎是一个红旗!麻烦排队所有这些门票!
  2. 为每个用户提供一个流程管理器,其可用性/团队和分配给该用户的票证。在这种情况下,我们通过“进程管理器查找”查询将查询替换为读取端,命令处理程序将调用Ticket.AssignTo(用户)。问题是我认为太多的业务逻辑泄露在域模型之外,特别是我们从User聚合中提取所有信息/模型以使其可用于查询
  3. 我倾向于使用第一个解决方案,它似乎更容易维护,修改/扩展和定位代码,但也许有些东西我不知道。

3 个答案:

答案 0 :(得分:2)

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

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

答案 1 :(得分: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;
}

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

答案 2 :(得分:-1)

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

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

在事件来源模型中,从来没有域存储库能够提供任何查询功能。 Yves Reynhout的AggregateSource是一个很好的参考,这里是IRepository interface。您可以轻松地看到没有&#34;查询&#34;这个界面中的方法无论如何。

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