CQRS事件采购:validation用户名唯一性

我们以一个简单的“账户注册”为例,这里是stream程:

  • 用户访问网站
  • 点击“注册”button并填写表格,点击“保存”button
  • MVC控制器:通过从ReadModel读取validation用户名的唯一性
  • RegisterCommand:再次validation用户名唯一性(这是问题)

当然,我们可以通过读取MVC控制器中的ReadModel来validationUserName的唯一性,以提高性能和用户体验。 但是, 我们仍然需要在RegisterCommand中再次validation唯一性 ,显然,我们不应该访问命令中的ReadModel。

如果我们不使用事件采购,我们可以查询领域模型,所以这不成问题。 但是如果我们使用Event Sourcing,我们无法查询域模型,那么我们如何validationRegisterCommand中的UserName唯一性?

注意:用户类有一个Id属性,UserName不是User类的关键属性。 使用事件采购时,我们只能通过Id获取域对象。

顺便说一句:在要求中,如果input的用户名已被占用,网站应该向访问者显示错误信息“对不起,用户名XXX不可用”。 显示一条消息,例如“我们正在创build您的帐户,请等待,我们将通过电子邮件向您发送注册结果”,这是不能接受的。

有任何想法吗? 非常感谢!

[UPDATE]

一个更复杂的例子:

需求:

下订单时,系统应该检查客户的订单历史,如果他是一个有价值的客户(如果客户在过去一年每月至less有10个订单,他是有价值的),我们使订单10%的折扣。

执行:

我们创buildPlaceOrderCommand,并在命令中,我们需要查询订购历史logging,看看客户是否有价值。 但是我们该怎么做呢? 我们不应该在命令中访问ReadModel! 正如Mikael 所说 ,我们可以在帐户注册的例子中使用补偿命令,但是如果我们在这个sorting的例子中也使用补偿命令,它会太复杂,代码可能太难维护。

如果您在发送命令之前使用读取模型validation用户名,我们正在讨论一个几百毫秒的竞态条件窗口,在这个窗口中可能会发生一个真正的竞争条件,这在我的系统中不会被处理。 与处理它的成本相比,这是不太可能发生的。

但是,如果你觉得你必须处理它,或者你只是想知道如何掌握这种情况下,这是一个方法:

使用事件源时,您不应该从命令处理程序或域访问读取模型。 但是,您可以执行的操作是使用域服务,该服务将再次侦听您在其中再次访问读取模型的UserRegistered事件,并检查用户名是否仍然不重复。 当然,您需要在这里使用UserGuid,并且您的读取模型可能已经用您刚刚创build的用户进行了更新。 如果发现重复,则可以发送补偿命令,例如更改用户名并通知用户该用户名已被占用。

这是解决问题的方法之一。

正如您可能看到的那样,以同步的请求 – 响应方式执行此操作是不可能的。 为了解决这个问题,我们使用SignalR来更新用户界面,只要有一些我们想要推送给客户端(如果他们仍然连接,那就是)。 我们所做的是让Web客户端订阅包含对于客户立即查看的信息的事件。

更新

对于更复杂的情况:

我会说订单放置不那么复杂,因为您可以在发送命令之前使用读取模型来确定客户端是否有价值。 其实,你可以查询,当你加载订单,因为你可能要显示客户,他们会得到10%的折扣之前,他们下订单。 只需在PlaceOrderCommand添加一个折扣,这也许是折扣的一个原因,这样您就可以跟踪为什么您要降低利润。

但是再一次,如果您在出于某种原因订单被放置之后确实需要计算折扣,那么再次使用一个会监听OrderPlacedEvent的域服务,在这种情况下,“补偿”命令可能是DiscountOrderCommand或其他东西。 该命令将影响Order Aggregate根,并且信息可能会传播到您的读取模型。

对于重复的用户名称情况:

您可以发送ChangeUsernameCommand作为来自域服务的补偿命令。 甚至更具体的东西,这将描述用户名更改的原因,这也可能导致创build一个Web客户端可以订阅的事件,以便您可以让用户看到用户名是重复的。

在域服务上下文中,我会说你也有可能使用其他方式来通知用户,比如发送一个可能有用的邮件,因为你不知道用户是否仍然连接。 也许这个通知function可能是由Web客户端订阅的事件启动的。

当谈到SignalR时,我使用一个SignalR Hub,用户在加载某个表单时连接到它。 我使用了SignalR Groupfunction,它允许我创build一个名为我在命令中发送的Guid值的组。 这可能是你的情况userGuid。 然后,我有事件处理程序订阅可能对客户端有用的事件,当事件到达时,我可以在所有客户端上调用一个JavaScript函数(在这种情况下,将只有一个客户端在您的创build重复的用户名案件)。 我知道这听起来很复杂,但事实并非如此。 我已经在一个下午成立了。 SignalR Github页面上有很多文档和例子。

我认为你们还没有把心态转移到最终的一致性和事件采购的本质上。 我有同样的问题。 具体而言,我拒绝接受您应该信任客户的命令,使用您的示例,如果没有域validation折扣应该继续,请说“以10%的折扣下订单”。 有一件事对我来说真的很重要, 就是Udi自己对我说的话 (查看接受的答案的评论)。

基本上我意识到没有理由不相信客户, 读取方面的所有内容都是从域模型生成的,所以没有理由不接受这些命令。 无论在阅读方面,说客户有资格获得折扣,这个领域已经放在那里。

顺便说一句:在要求中,如果input的用户名已被占用,网站应该向访问者显示错误信息“对不起,用户名XXX不可用”。 显示一条消息,例如“我们正在创build您的帐户,请等待,我们将通过电子邮件向您发送注册结果”,这是不能接受的。

如果您要采用事件采购和最终一致性,则需要接受有时在提交命令后不能立即显示错误消息。 使用唯一的用户名示例,发生这种情况的可能性非常小(在发送命令之前检查读取方)它不值得担心太多,但是需要为此scheme发送后续通知,或者可能询问他们在下次login时使用不同的用户名。 关于这些情况的好处是,它让你思考商业价值和真正重要的事情。

更新:2015年10月

只是想补充一下,实际上,在面向公众的网站方面 – 表明已经采取了电子邮件实际上是违反安全最佳实践。 相反,注册似乎已经成功通知用户已经发送了validation电子邮件,但是在用户名存在的情况下,电子邮件应该通知他们并提示他们login或重置密码。 虽然这只适用于使用电子邮件地址作为用户名,我认为这是明智的。

创build一些立即一致的读取模型(例如,不在分布式networking上)在与命令相同的事务中得到更新没有任何问题。

阅读模型最终在分布式networking上保持一致有助于支持扩展读取系统的读取模型。 但没有什么可说的,你不能有一个特定领域的读取模型,立即一致。

立即一致的读取模型仅用于在发出命令之前检查和接收数据(实际上它是对命令的服务),您不应该使用它直接向用户显示读取数据(即从GET web请求或类似)。 最终使用consitent,可扩展的读取模型。

我觉得对于这样的情况,我们可以用一个机制,像“顾问locking到期”。

样例执行:

  • 检查用户名是否存在于最终一致的读取模型中
  • 如果不存在; 通过使用像keyvalue存储或cachingredis-couchbase; 尝试推送用户名作为关键字段到期。
  • 如果成功; 然后引发userRegisteredEvent。
  • 如果在读取模型或caching存储中存在用户名,请通知访问者该用户名已被占用。

即使你可以使用一个SQL数据库; 插入用户名作为某个locking表的主键; 然后一个预定的工作可以处理到期。

像许多其他实施基于事件源的系统一样,我们遇到了唯一性问题。

起初,我是一个支持者,让客户端在发送命令之前访问查询端,以确定用户名是否唯一。 但是后来我发现有一个没有validation唯一性的后端是一个坏主意。 为什么在可能发布会破坏系统的命令时执行任何操作? 后端应validation所有的input,否则你打开不一致的数据。

我们所做的是在命令端创build一个index 。 例如,在简单的用户名必须是唯一的情况下,只需创build一个用户名字段的UserIndex。 现在命令端可以检查用户名是否已经在系统中。 命令执行完成后,将新的用户名存储在索引中是安全的。

类似的东西也可以用于订单折扣问题。

好处是您的命令后端可以正确validation所有input,因此不会存储不一致的数据。

一个缺点可能是您需要对每个唯一性约束进行额外的查询,并且执行额外的复杂性。

你有没有考虑使用“工作”caching作为一种RSVP? 这很难解释,因为它在一个循环中工作,但基本上,当一个新的用户名被“声称”(即,发出命令来创build它)时,你把用户名放在caching中,足够长的时间来解释通过队列的另一个请求,并且非规范化到读取模型中)。 如果它是一个服务实例,那么在内存中可能会工作,否则集中Redis或什么。

然后,当下一个用户填写表单(假设有一个前端)时,您可以asynchronous检查读取模型的用户名是否可用,并提醒用户是否已经使用。 当提交命令时,你检查caching(不是读取模型),以便在接受命令之前validation请求(在返回202之前); 如果名称在caching中,则不要接受该命令,如果不是,则将其添加到caching中; 如果添加失败(重复键,因为其他进程击败了它),然后假设名称被采取 – 然后适当地响应客户端。 在这两件事之间,我不认为会有很多碰撞的机会。

如果没有前端,那么您可以跳过asynchronous查找,或者至less让您的API提供端点来查找它。 你真的不应该让客户端直接与命令模型直接对话,而在它前面放置一个API将允许你让API充当命令和读主机之间的中介。