C

CRUD 中没有 U

2025-06-09

CRUD 中没有 U

这篇文章最初发表在我的博客上

REST 是基于资源(以 URI 表示)的概念构建的。指定 HTTP 动词和资源 URI 进行 HTTP 调用,会对指定的资源执行相应的操作。大多数 REST 框架都提供生成器,您只需指定一个资源名称,框架就会围绕该名称生成相应的脚手架。然而,许多生成器都默认使用 CRUD 模型(创建、读取、更新、删除)作为起点。资源被定义为一系列属性,使用类似 JSONSchema 或特定语言的数据对象定义,然后生成用于创建、读取、更新和删除该资源的方法存根。

虽然为开发人员提供一个工作起点是件好事,但我对使用 CRUD 作为 API 的起点有很大意见。CRUD 中的“U”是我最不喜欢的,尽管我不喜欢其他字母,这取决于具体用例。不过,我们来谈谈“U”。通用更新方法允许客户端更新资源的任何字段,然后用新版本覆盖现有版本。但是,如果您允许客户端这样做,那么您的服务 API 在其所使用的任何底层数据存储之上都几乎无法提供任何价值。服务层的关键增值功能之一是在底层数据之上强制执行业务约束,而资源最终总是会受到业务约束。

但是我们不能在更新方法中添加业务约束吗?让我们以一个简单的银行账户资源为例,看看会发生什么。首先,客户端不应该能够调用 API 并将他们的账户余额随意更新到他们想要的数值。账户可能存在最低余额限制。好的,所以你向更新方法添加了一些检查,例如,如果账户余额发生变化,它必须在指定的范围内。问题解决了吗?嗯,并没有。任何余额调整都应该记录为某种交易,对吗?这是贷记?借记?转账?如果客户端尝试更改账号怎么办?这是否被允许?这会破坏其他数据关系吗?不难看出,我们提出的问题越多,更新方法实现就越容易变成意大利面条式代码。我见过一些团队走这条路,他们的代码试图从更改的字段推断客户端的意图,最终代码变得一团糟。

那么还有什么替代方案呢?就我个人而言,我非常推崇使用领域驱动设计 (DDD)来设计任何类型的 API。DDD 所基于的理念是,软件应该根据正在解决的实际问题进行建模。它创建了一种语言,用于根据称为实体聚合的关键业务对象来描述软件。它还定义了服务值对象存储库等术语,它们协同工作以解决特定业务领域中的问题,用 DDD 术语来说就是有界上下文。您不必使用 REST 即可使用 DDD,但是,我发现它与 REST API 配合得特别自然,因为 REST 资源可以很好地映射到 DDD 实体。

那么这一切意味着什么呢?这意味着你的 API 应该以领域对象及其提供的业务操作为中心。业务操作是通用更新方法及其所有缺陷的关键替代方案。让我们用前面提到的银行示例来说明。

对于银行 API,一个显而易见的领域对象(用 DDD 术语来说,或者叫实体)就是账户,它代表一个银行账户。与其遵循账户的 CRUD 模型,不如定义一些对银行账户有意义的具体业务操作。以下是一组不错的入门级写入操作:

  1. 开设——开设新账户。
  2. 关闭——关闭现有帐户。
  3. 借记——从账户中扣除资金。
  4. 信用——向账户中存钱。

这些操作是特定的,可以强制执行某些业务约束。例如,我们可能不想允许贷记已关闭的账户,并且可以在借记操作中强制执行最低余额检查。在读取方面,我们还可以提供与客户用例匹配的特定查询:

  1. 加载——通过账户 ID 加载单个账户。
  2. 交易历史 - 列出账户的交易历史。
  3. 客户账户 - 列出给定客户 ID 的账户。

现在我们知道了我们的业务运营是什么,下面是将它们映射到 REST API 的示例:

  1. POST /account-开设新账户。
  2. PUT /account/<accountId>/close - 关闭现有帐户。
  3. PUT /account/<accountId>/debit - 从账户中取出钱。
  4. PUT /account/<accountId>/credit - 向账户中存钱。
  5. GET /account/<acountId> - 通过账户 ID 加载单个账户
  6. GET /account/<accountId>/transactions - 列出帐户的交易历史记录。
  7. GET /accounts/query/customerId/<customerId> - 列出给定客户 ID 的帐户。

这看起来与基本的 CRUD API 大相径庭,但关键在于允许的操作是具体且定义明确的。这将为服务实现者和客户端带来更佳的体验。服务实现不再需要猜测哪些属性的更新会暗示哪些业务操作。相反,业务操作是明确的,这使得代码更简洁、更易于维护。在客户端,哪些操作可以执行、哪些操作不能执行会更加清晰。如果 API 文档完善,例如使用Swagger定义,那么每个 API 的约束也会非常清晰。

以这种方式定义 API 比简单的 CRUD 生成器需要更多的前期思考,但我认为这是一件非常好的事情。如果您计划将 API 作为公共端点公开,那么您将不得不在很长一段时间内支持该 API。按照软件标准,这基本上可以视为永久支持。我总是鼓励团队预先花时间处理那些以后难以更改的事情,API 就是我给出的第一个例子。

因此,不要急于遵循服务 API(REST 或其他)的 CRUD 模型。相反,应该使用 DDD,根据领域对象及其可执行的业务操作来定义 API。

如果您想了解更多基于领域对象定义 API 的示例,我建议您查看 Amazon Web Services API。查阅任何服务的开发者指南,它们应该以“关键概念”或类似标签的部分开头。在那里,它们描述了该服务的概念领域对象。例如,S3 定义了存储桶、对象和权限等对象。Kinesis 则拥有流和分片。一旦您了解了服务的领域对象,请查看 API 参考并浏览该服务的 API 列表。您会注意到,API 是围绕这些领域对象构建的,这使得它更容易理解和使用。

希望这有帮助!

鏂囩珷鏉ユ簮锛�https://dev.to/jlhcoder/there-is-no-u-in-crud
PREV
Android 的十亿美元错误 十亿美元错误引言 Android 的十亿美元错误 💣 LeakCanary 🐤 Android 的 Burrito 设计模式是怎么诞生的?它的文档和示例至今仍然很糟糕 GenAI LIVE!| 2025 年 6 月 4 日
NEXT
什么是 CSS 变量?