CQRS Event Sourcing 检查用户名在发送命令时是否来自 EventStore 是唯一的

CQRS Event Sourcing check username is unique or not from EventStore while sending command

当我们有特定的唯一 EntityID 时,EventSourcing 工作得很好,但是当我试图从 eventStore 获取信息而不是特定的 EntityId 时,我遇到了困难。

我正在将 CQRS 与 EventSourcing 结合使用。作为事件源的一部分,我们将事件存储在 SQL table 中作为列(EntityID(uniqueKey),EventType,EventObject(例如。UserAdded))。

因此,在存储 EventObject 时,我们只是序列化 DotNet 对象并将其存储在 SQL 中,因此,与 UserAdded 事件相关的所有详细信息都将采用 xml 格式。我担心的是我想确保数据库中存在的用户名应该是唯一的。

所以,在执行 AddUser 命令时,我必须查询 EventStore(sql db) 特定的用户名是否已经存在于 eventStore 中。 所以我要这样做需要序列化事件存储中的所有 UserAdded/UserEdited 事件并检查请求的用户名是否存在于事件存储中。

但是作为CQRS命令的一部分不允许查询可能是因为Race condition.

所以,我在发送 AddUser 命令之前尝试只查询 eventStore 并通过序列化所有事件 (UserAdded) 获取所有用户名并获取用户名,如果请求的用户名是唯一的,则执行命令否则抛出异常该用户名已存在。

与上述方法一样,我们需要查询整个数据库,我们可能有数十万个 events/day。因此 query/deserialization 的执行将花费很多时间,这将导致性能问题。

我正在寻找更好的方法approach/suggestion通过从 eventStore 或任何其他方法获取所有用户名来维护用户名的唯一性

因此,您的客户端(发出命令的东西)应该完全相信它发送的命令将被执行,并且它必须通过确保在发送 RegisterUserCommand 之前没有其他用户是使用该电子邮件地址注册。换句话说,您的客户端必须执行验证,而不是您的域或域周围的应用程序服务。

来自http://cqrs.nu/Faq

This is a commonly occurring question since we're explicitly not performing cross-aggregate operations on the write side. We do, however, have a number of options:

Create a read-side of already allocated user names. Make the client query the read-side interactively as the user types in a name.

Create a reactive saga to flag down and inactivate accounts that were nevertheless created with a duplicate user name. (Whether by extreme coincidence or maliciously or because of a faulty client.)

If eventual consistency is not fast enough for you, consider adding a table on the write side, a small local read-side as it were, of already allocated names. Make the aggregate transaction include inserting into that table.

通常,没有正确答案,只有适合您领域的答案。

您是否处于真正需要即时一致性的环境中?在通过查询(例如,在客户端)检查唯一性与处理命令之间创建相同用户名的几率是多少?例如,您的领域专家会容忍 100 万分之一的用户名冲突(之后可以补偿)吗?您首先会拥有一百万用户吗?

即使需要即时一致性,"user names should be unique"...在哪个范围内?一个 Company ?一个 OnlineStore ?一个 GameServerInstance ?您能否找到唯一性约束必须保持的最受限范围,并使该范围成为新用户萌芽的聚合根?如果聚合根使这些事件变得小而简单,为什么 "replay all the UserAdded/UserEdited events" 解决方案毕竟会很糟糕?

使用 GetEventStore(来自 Greg Young),您可以使用任何字符串作为 aggregateId/StreamId。使用用户名作为聚合的 id 而不是 guid,或者像 "mycompany.users.john" 这样的组合作为键和..瞧!您有免费的用户名唯一性!

作为业务逻辑的一部分,在写入操作中使用存储库查询不同的聚合是不被禁止的。您可以这样做,以便通过使用某些域服务(cross-aggregate 操作)接受命令或由于重复用户而拒绝它。 Greg Young 在这里提到了这一点:https://www.youtube.com/watch?v=LDW0QWie21s&t=24m55s

在正常情况下,您只需要查询所有 UserCreated + UserEdited 事件即可。 如果您希望每天有数千个这样的事件,那么您的事件可能会变得臃肿,您应该更加原子化地进行设计。例如,不是每次用户发生某些事情时都引发 UserEdited 事件,而是考虑使用 UserPersonalDetailsEditedUserAccessInfoEdited 或类似的,其中必须唯一的字段与其他字段的处理方式不同用户字段。这样,在接受或不接受命令之前查询所有 UserCreated + UserAccessInfoEdited 将是一个更轻松的操作。

我个人会采用以下方法:

  1. 事件中的更多原子性,以便更明确地描述涉及应该全局唯一的字段的所有内容(例如:UserCreatedUserAccessInfoEdited
  2. 在写入端提供可用的投影,以便在写入操作期间查询它们。因此,例如,我会订阅所有 UserCreatedUserAccessInfoEdited 事件,以便保留具有所有唯一字段(例如电子邮件)的可查询 "table"。
  3. CreateUser 命令到达域时,域服务将查询此电子邮件 table 并接受或拒绝该命令。

此解决方案有点依赖于最终一致性,并且有可能查询告诉我们该字段未被使用并允许命令成功引发事件 UserCreated 而实际上投影并未被使用尚未从以前的事务更新,因此导致系统中有 2 个字段不是全局唯一的情况。

如果您想完全避免这些不确定情况,因为您的业务无法真正处理最终一致性,我的建议是通过将它们显式建模为您无处不在的语言的一部分来在您的域中处理此问题。例如,您可以对聚合进行不同的建模,因为很明显您的聚合用户并不是您的交易边界(即:它取决于其他人)。