如何在同时使用REST、CQRS和EventSourcing时支持REST命令?

18

考虑以下粗粒度的REST API,用于联系人资源

POST          /api/contacts                             
GET           /api/contacts                             
GET           /api/contacts/:id                         
PUT           /api/contacts/:id                         
DELETE        /api/contacts/:id                         

考虑为联系资源使用事件溯源,即验证命令并存储事件。因此,每个事件都必须被存储,包括每个字段级别的更改。

CreateContactCommand -> | Contact("john", "doe", 25) | -> ContactCreatedEvent
FirstNameChangeCommand -> | Contact("jane", "doe", 25) | -> FirstNameChangedEvent
LastNameChangeCommand -> | Contact("jane", "dear", 25) | -> LastNameChangedEvent
AgeChangeCommand -> | Contact("jane", "doe", 30) | -> AgeChangedEvent

现在,将REST和EventSourcing结合起来。

在进行REST时,客户端如何与标准的REST API通信以对字段级别的更改生成命令并在服务器端REST终点执行?

主要问题是,如何设计REST API,使其最终还能支持事件溯源?

如果有人能够提供帮助,那将不胜感激。


相关链接:https://dev59.com/Zorda4cB1Zd3GeqPNop5 - Mark Seemann
你为什么要大声喊叫(用粗体字写)? - Sir Rufo
2个回答

11

CQRS和事件溯源既不是API设计原则,也不是顶层架构。但是,如果您想将API“公开”为基于任务的API,您可以将链接作为“联系人”资源的一部分公开。

GET /contacts/1234

响应

200 OK
<contact>
  <atom:link href="/contacts/1234/first-name" rel="first-name" />
  <atom:link href="/contacts/1234/last-name" rel="last-name" />
  <atom:link href="/contacts/1234/age" rel="age" />
  <first-name>Jane</first-name>
  <last-name>Doe</last-name>
  <age>25</age>
</contact>

假设您将API更改为true级别3 REST API。

此外,/contacts/1234仅接受GETDELETE不允许PUT)请求。如果客户端想要更改联系人的名字等信息,则必须遵循关系类型为first-name的链接,并对该资源进行PUT请求:

PUT /contacts/1234/first-name
<first-name>John</first-name>

如果这里不是 first-name 字段,那么其他任何东西在这里使用的PUT命令都会被忽略或拒绝。

因此,当服务收到针对 first-name 资源的PUT请求时,这就是更改联系人名字的命令。

尽管如此,这仍然不是一个合适的基于任务的API,因为它没有捕获为什么更改名字的原因,但我希望你能明白这个想法。


感谢马克点击这个想法。这将帮助我开始。 - PainPoints
1
+1 到“这不是一个合适的基于任务的用户界面”。很有可能,您的命令和事件对于系统的健康运行来说过于细粒度化。 - guillaume31
1
那么,如果您需要更改名字和姓氏,您必须生成2个请求吗?此外,您必须单独处理每个请求。编写通用处理程序将导致实现某种PATCH方法...看起来至少很奇怪。 - S2201
1
@Savash 这篇文章主要是关于DDD、CQRS和基于任务的API。正如我在回答中所提到的,这个例子并不是真正的基于任务的,但我试图以一种易于理解的方式表达一个观点。这并不是关于更改名字或姓氏,而是关于提供关于为什么事物会发生变化的信息。如果您有理由更改名字和姓氏,也可以为此提供语义链接,但请记住,这不是CRUD。 - Mark Seemann
@MarkSeemann 你不觉得通常应该一起更改的属性被分组到模型中,并为此创建语义链接,从而导致我们使用相同的REST吗? - S2201
@Savash 这是肯特·贝克的一个经验法则:一起变化的事物应该放在一起;不同速度变化的事物不应该放在一起...(从记忆中改述)。 - Mark Seemann

0

REST是一种分布式应用架构风格,适用于Web。Web有自己的规则和限制,它是表示和资源的领域,而不是任务、命令或查询。

Web比您的应用程序低级。在设计HTTP资源时,您必须考虑缓存、版本控制、可重复性、性能、松散耦合等Web上下文中的因素,因此您可能不想完全匹配域实体,更不用说这些实体的属性了。此外,HTTP具有非常有限的动词,当动词无法满足您的需求时,资源并不一定是描述任务或操作的最佳选择。

因此,在您的域中有意义的命令与(资源、动词)对之间的严格一对一对应并不总是理想或充分的。您可能希望进一步设计自己的域应用程序协议,以便REST客户端根据更精细的规则与服务器通信。 DAP反映在超媒体链接和转换关系中,正如Mark所指出的那样,但您还可以使用自定义内容类型来更好地描述请求或响应中有效载荷的类型。例如,它们可以包含您正在发送的域命令的类型。

如果您查看本文, 您会发现在“资源”下的图表中,(POST, api/inventoryItem/{id}) 这对并不是唯一的。它可以用于传输RemoveItemsFromInventoryCommandCheckInItemsToInventoryCommand。您如何在HTTP请求级别上指定它,是通过使用自定义内容类型标头:Content-Type:application/json;domain-model=RemoveItemsFromInventoryCommandContent-Type:application/json;domain-model=CheckInItemsToInventoryCommand


嗨,guillaume31,我想我也有机会澄清一下。对于设计DAP,是将资源的行为暴露在像你提到的文章中那样的方式好呢?还是遵循Mark指出的Level 3 REST API设计?我应该选择对我来说更容易的方式吗? - PainPoints
1
该文章还描述了一个三级REST API。马克说的一切仍然有效。问题是,你的命令不能捕捉用户意图(基于任务的UI)在我看来。它们非常细粒度,到达字段级别。因此,你被迫将每个实体字段塞进资源中,这我认为是不切实际的,或者找到另一种方式,例如通过媒体类型。但两者都将保持在Richardson REST成熟度模型的第三级。 - guillaume31
将命令名称放在URL或Content-Type标头中,它们有何不同?两者都与传输状态无关。 - Arnaud Le Blanc
@arnaud576875 这是不同的,因为在URI中放置命令名称意味着它是不同的资源。此外,REST不是关于传输状态,而是传输表示,这些表示捕获该资源的当前或预期状态。当然,我给出的示例捕获了预期的状态。它们的有效载荷包含资源未来状态的一部分,即{ count: X } - guillaume31
@arnaud576875,你肯定可以和我们分享你自己对这个问题的理想解决方案吧? - guillaume31
在自定义标头中指定命令类型怎么样?这样做可以接受吗?有什么主要缺点吗? - edu_

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接