事件源和REST


17

我遇到了事件源设计,我想在需要REST客户端(准确地说是RESTful)的应用程序中使用。但是,由于REST非常像CRUD,并且事件源基于任务,因此我无法将它们连接在一起。我想知道您如何设计基于对REST服务器的请求的命令创建。考虑以下示例:

借助REST,您可以将一个新状态放入名为File的资源中。在一个请求中,您可以发送新的文件名,可以更改父文件夹和/或更改文件的所有者,依此类推。

如何构造服务器,以便可以使用事件源。我在考虑这些可能性:

  1. 确定服务器上哪些字段被改变,并创建相应的命令(RenameFileCommandMoveFileCommandChangeOwnerCommand,...)并单独派遣这些。但是,在这种设置中,每个命令都可能失败,从而使其他命令无法进行事务处理,从而无法进行资源的“原子”更改。

  2. 调度只需要一个命令(UpdateFileCommand),并在命令处理程序,更精确地在总量上,确定哪些领域发生了变化,并发送单个事件,而不是(FileRenamedEventFileMovedEventOwnerChangedEvent,...)

  3. 我一点都不喜欢这个命令:在对服务器的请求中,我会在标头中指定要使用的命令,因为UI仍基于任务(但通信是通过REST完成的)。但是,在REST通信的任何其他用途(例如,外部应用程序)中,它都将失败,因为它们不一定要仅更改一个请求中的一个字段。另外,我在UI,REST和基于ES的后端中引入了很大的结合。

您更喜欢哪一个,或者有更好的方法来解决这一问题?

旁注:使用Java和Axon Framework编写的用于事件源的应用程序。


当然不是第三名。我会做第一,但我必须考虑一下。
inf3rno

您是否对单个HTTP请求是否会导致多个命令有疑问?我听得懂吗 恕我直言。它应该可以这样做,但是我已经有一段时间没有读过DDD了,所以我必须检查一个有关如何实现这一点的示例代码。
inf3rno

我找到了一个示例,但这是CRUD。github.com/szjani/predaddy-issuetracker-sample/blob/3.0/src/hu/…我会问作者他的看法,他比我更了解DDD。
inf3rno

1
我的想法是,如果要立即保持一致性,则应在“工作单元”中使用多个命令。如果您正在谈论最终的一致性,那么这个问题对我来说就没有意义。发送CompositeCommand的另一种可能的解决方案,其中可以包含要原子执行的命令。它可以是一个简单的集合,唯一重要的是总线可以正确处理它。
inf3rno

1
据他介绍,您应该尝试在命令和HTTP请求之间实现1:1关系。如果您不能做到这一点(那是难闻的气味),那么您应该使用散装(我称之为复合材料)使其原子化。
inf3rno

Answers:


11

我认为您可能在此处有一个用户流程来实现不匹配。

第一:用户是否真的想同时对文件进行多项更改?重命名(可能包含或不包含路径更改?),所有权更改以及文件内容更改(出于争论的目的)似乎是单独的操作。

让我们以答案为“是”的情况为例-您的用户确实确实希望同时进行这些更改。

在这种情况下,我会强烈反对发送多个事件的任何执行- ,,RenameFileCommand -表示该用户意图。MoveFileCommandChangeOwnerCommand

为什么?因为事件可能失败。也许它极为罕见,但是您的用户提交了一个看起来很原子的操作-如果单个下游事件失败,则您的应用程序状态现在无效。

您还会在每个事件处理程序之间明确共享的资源上引发种族危险。您将需要以这样的方式编写“ ChangeOwnerCommand”:文件名和文件路径无关紧要,因为到接收命令时,它们可能已过期。

当实现通过移动和重命名文件来实现非事件驱动的静态系统时,我更喜欢通过使用eTag系统之类的方法来确保一致性-确保所编辑资源的版本是用户最后检索到的版本,如果失败,则失败从那时起已被修改。但是,如果您要为此单个用户操作调度多个命令,则需要在每个命令之后增加资源版本-因此您无法知道用户正在编辑的资源确实与上次阅读的资源版本相同。

我的意思是-如果其他人几乎同时在文件上执行其他操作,该怎么办?这6个命令可以按任何顺序堆叠。如果我们只有2个原子命令,则较早的命令可能会成功,而较晚的命令可能会失败“自上次检索以来,资源已被修改”。但是,如果命令不是原子的,则无法避免这种情况,因此会破坏系统一致性。

有趣的是,在REST中正在朝着基于事件的体系结构发展,这被称为“没有PUT的情况”,在2015年1月Thoughtworks技术雷达中被推荐。这里有一个相当长的博客,关于没有PUT的休息

本质上,想法是POST,PUT,DELETE和GET对于小型应用程序来说很好,但是当您需要开始假设如何在另一端解释put,post和delete时,就会引入耦合。(例如,“当我删除与我的银行帐户相关联的资源时,应关闭该帐户”),建议的解决方案是以一种更多基于事件的方式来处理REST。即让POST将用户意图作为单个事件资源。

另一种情况更简单。如果您的用户不想同时执行所有这些操作,请不要让他们这样做。为每个用户意图发布一个事件。现在,您可以在资源上使用etag版本控制。

至于其他使用与您的资源完全不同的API的应用程序。闻起来像麻烦。您可以在RESTful API之上构造旧API的外观并将它们指向该外观吗?即公开通过REST服务器对文件依次执行多次更新的服务?

如果您既未在旧解决方案之上构建RESTful接口,又未在REST解决方案之上构建旧接口的外观,并且尝试维护这两个指向共享数据资源的API,那么您将遇到很多麻烦。


我可以看到不匹配,但是,从原则上来说,REST是可以通过将新状态放入资源(通过某种表示)来更新多个字段的状态的。REST就是这样定义的-表示状态转移,缺点是在过程中失去了用户的意图。此外,REST几乎是外部API的标准。我只想设计该应用程序,以便可以同时使用两者。由于ES,删除PUT就像解决方法。据我所知,只要命令和事件处理在一个事务中,一个发出多个事件的更新命令就是可行的。
红发

无论如何,Etag是我想做的事情。另外,您指向“更长的博客文章”的链接与第一个链接相同。
红发

经过一番考虑之后,我将重新考虑这一点,然后再提出。当然,删除PUT不仅是“ ES的解决方法”。我已经修复了博客链接。
完美主义者

4

刚才,我遇到了下一篇文章,该文章鼓励在Content-Type标头中向服务器指定请求中的命令名称(同时遵循5种级别的媒体类型)。

在本文中,他们提到RPC样式不利于REST,并建议扩展Content-Type以指定命令名称:

一种常见的方法是使用RPC样式的资源,例如/ api / InventoryItem / {id} / rename。尽管这似乎消除了对任意动词的需求,但它与REST的面向资源的表示形式背道而驰。需要提醒我们的是,资源是名词,HTTP动词是动词/动作,自描述消息(REST的宗旨之一)是传达信息和意图的其他轴的工具。实际上,HTTP消息有效负载中的命令应足以表达任何任意操作。但是,依靠消息的主体本身存在问题,因为消息主体通常以流的形式传递,并且在识别动作之前总是对消息主体进行整体缓冲,这并不总是可能的,也不是明智的。

PUT /api/InventoryItem/4454c398-2fbb-4215-b986-fb7b54b62ac5 HTTP/1.1
Accept:application/json, text/plain, */*
Accept-Encoding:gzip,deflate,sdch
Content-Type:application/json;domain-model=RenameInventoryItemCommand`

文章在这里:http : //www.infoq.com/articles/rest-api-on-cqrs

您可以在此处阅读有关5级媒体类型的更多信息:http : //byterot.blogspot.co.uk/2012/12/5-levels-of-media-type-rest-csds.html


尽管他们将域事件公开给REST API(我认为这是一种不好的做法),但我还是喜欢该解决方案,因为它不会为CQRS单独创建新的“协议”,无论是在正文中还是在其他地方发送命令名称标头,并遵循RESTful原则。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.