【问题标题】:Persisting multiple things a single command handler with CQRS使用 CQRS 持久化单个命令处理程序
【发布时间】:2018-02-06 20:13:57
【问题描述】:

我正在尝试将 CQRS+ES 应用于我的宠物项目。但我不确定如何处理复杂的命令。

假设我有一个网页,您可以在其中创建一个新的User。因此,在您的页面上,您只需输入名字、姓氏、用户名和密码。但是,您必须还向该用户添加一个或多个Roles。当点击 Save 按钮时,会触发以下命令CreateUserWithRolesCommand

以下是命令处理程序中的有效方法吗?

public class CreateUserWithRolesCommandHandler : ICommandHandler<CreateUserWithRolesCommand>
{
    private readonly AppDbContext _context;

    public UserCommandHandler(AppDbContext context)
    {
        _context = context;
    }

    public void Handle(CreateUserCommand command)
    {
        // todo: begin db transaction 

        var user = new User();
        user.Username = command.Username;
        user.Password = command.Password;
        user.Firstname = command.Firstname;
        user.Lastname = command.Lastname;
        _context.User.Add(user);
        _context.Save();

        // After save, get user id
        van userId = user.Id;

        van userRoles = new UserRoles;

        // Ommiting foreach loop and just taking the 
        // first role to keep the example simpler
        userRole.RoleId = command.Roles.First().RoleId;
        userRole.UserId = userId;
        _context.UserRoles.Add(userRole);
        _context.Save();

        // end db transaction and commit if all successful
    }
}

【问题讨论】:

    标签: design-patterns domain-driven-design cqrs


    【解决方案1】:

    我看到的第一件事是你贫血的领域模型。你只有二传手,这是不行的。用命令方法替换所有设置器。在这种情况下,您应该只有一个返回 void 的 User.create(usename, password, firstName, lastName) 方法。

    其次,有两个聚合,因此您需要有两个事务。在您的代码中,您只有一笔交易。请记住Aggregates are the largest transactional boundary

    但是您考虑到在第二次事务(将角色添加到用户)之前可能(并且将会)发生一些不好的事情。例如,在将用户添加到存储库后服务器重新启动或崩溃。重新启动后,它将没有足够的信息来继续添加用户角色的过程

    一种解决方案是将其建模为Saga/Process manager。您将拥有一个使用所有需要的信息创建的 CreateUserWithRoles 实体。在这种情况下, CreateUserCommand 的内容就足够了。然后,您需要添加一个progress 状态变量,即一个将记住上次执行状态和/或创建 User.create 和 UserRoles.add 的 Enum(Started、UserCreated 和 RoleAdded)幂等的。创建 CreateUserWithRoles 实体后,您 run 它。 run 方法通过查看progress 跳过已执行的步骤并执行剩余的步骤。这样,如果发生了不好的事情(相信我,它会的),S​​aga 可以重新开始。

    您还需要一种方法来检测所有处于停止状态的 Sagas 并恢复它们(通过执行它们的 run 方法)。

    PS:我使用术语“事务”来表示操作必须以原子模式(全有或全无)完成,但可扩展的事件存储实现根本不应该使用数据库事务。 em>

    【讨论】:

    • 确实与康斯坦丁讨论了如何处理最终一致性。但是我认为对于这种特定场景“CreateUserWithRole”和使用 ES 来说有点过分了。我的建议只是将事件发布到同一个事件流(userId),一个是“UserRegistered”,另一个是“RolesAdded”。这两个事件在同一个事务中保存到事件存储中。
    • @martinezdelariva 我使用了“事务”这个词,但我不一定指数据库事务。在 ES 中,可以使用无事务持久性(以获得更好的性能和可伸缩性)。但这仅适用于单个流。两个聚合有 2 个流,因此不能在单个“事务”中完成
    【解决方案2】:

    您在命令处理程序中所做的一切都很好。但是,您似乎正在使用面向数据的类(可能是实体框架)。您的域模型通常会映射自任何数据存储机制(甚至是 ORM,除非您的 ORM 能够直接使用域模型)。我尽量避免使用 ORM。接下来是 CQRS 还没有真正在图片中。这也不符合事件溯源的条件,因为您的聚合不是由事件构成的。

    我有一个正在进行的工作(截至 2018 年 2 月 7 日)处理 身份和访问控制,名为 Shuttle.Access,它使用了我的 Shuttle.Recall 事件源机制。 domain source 可能会给你一些想法。

    您的基本设计看起来不错。一个 User 聚合与许多附加的 UserRole 值对象。

    【讨论】:

      猜你喜欢
      • 2021-06-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多