不要在REST API中公开您的JPA实体 - Thorben Janssen


在REST API中公开实体,还是使用DTO类?(banq注:如果了解单一职责或DDD和Clean架构,基础设施应该和业务逻辑分离,API JPA等属于不同的基础设施,应该都和领域对象分离)
这些问题以及由此引发的所有讨论有两个主要原因:

  1. 实体是POJO。通常看起来,它们可以轻松地序列化和反序列化为JSON文档。如果真的很容易实现,那么REST端点的实现将变得非常简单。
  2. 在API公开实体会和持久性模型之间建立牢固的耦合。两种模型之间的任何差异都会带来额外的复杂性,因此您需要找到一种弥合两者之间差距的方法。不幸的是,您的API和持久性模型之间总是存在差异。最明显的是实体之间关联的处理。

即使很容易公开您的实体,但对于复杂度至少中等以上的所有应用程序以及需要长时间支持的所有应用程序,都应避免公开实体。在API上公开实体使您无法在设计API时实现一些最佳做法;它降低了实体类可读性,减慢了应用程序的速度,并使实现真正的REST体系结构变得困难。
可以通过设计DTO类来避免所有这些问题,然后在API上进行序列化和反序列化。这要求您在DTO和内部数据结构之间实现映射。但是,如果您考虑在API中公开实体的所有弊端,那是值得的。

隐藏实施细节
作为一般的最佳实践,您的API不应公开您应用程序的任何实现细节。您用来保留数据的结构就是这样的细节。在您的API中公开您的实体显然不遵循这种最佳做法。
几乎每次我在讨论中提出这个论点时,都会有人怀疑地抬起眉毛或直接问这是否真的有那么大的意义。
好吧,如果您希望能够在不更改API的情况下添加,删除或更改实体的任何属性,或者要在不更改数据库的情况下更改REST终结点返回的数据,这只是一个大问题。
换句话说:是的,将API与持久层分开是实现可维护应用程序所必需的。如果您不这样做,则REST API的每次更改都会影响您的实体模型,反之亦然。这意味着您的API和持久层将不再能够彼此独立地发展。

不要使用其他注释来夸大您的实体
而且,如果您考虑仅在实体与REST端点的输入或返回值完美匹配时才公开实体,那么请注意,您需要为JSON序列化和反序列化添加其他注释。
大多数实体映射已经需要几个注释。为您的JSON映射添加其他类会使实体类更加难以理解。最好保持简单,将实体类与用于序列化和反序列化JSON文档的类分开。

API和JPA处理关联关系不同
不在API中公开实体的另一个论点是实体之间关联的处理。持久层和API对待它们的方式有所不同。如果要实现REST API,则尤其如此。
对于JPA和Hibernate,通常使用由实体属性表示的托管关联。这样一来,您就可以轻松地在查询中加入实体,并可以使用实体属性遍历业务代码中的关联。根据配置的访存类型和查询,此关联可以完全初始化,也可以在第一次访问时延迟获取。
在REST API中,您以不同的方式处理这些关联。正确的方法是为每个关联提供一个链接。罗伊·菲尔丁(Roy Fielding)将其描述为HATEOAS。它是REST体系结构的重要组成部分之一。但是大多数团队决定要么根本不对关联建模,要么只包括​​id引用。
链接和ID参考提供了类似的挑战。将实体序列化为JSON文档时,您需要获取关联的实体并为每个实体创建引用。在反序列化期间,您需要获取引用并为其获取实体。根据所需查询的数量,这可能会使您的应用程序变慢。
这就是为什么团队在序列化和反序列化期间经常排除关联的原因。这对于您的客户端应用程序可能是可以的,但是如果您尝试合并通过反序列化JSON对象创建的实体,则会造成问题。Hibernate期望托管关联引用其他实体对象或动态创建的代理对象或特定于Hibernate的List或Set实现。但是,如果您反序列化JSON对象并忽略实体上的托管关联,则关联将设置为null。然后,您需要手动设置它们,否则Hibernate将从数据库中删除该关联。
如您所见,管理关联可能很棘手。不要误会我的意思;这些问题都可以解决。但这需要额外的工作,而且如果您仅忘记其中之一,则会丢失一些数据。

设计您的API
公开API的另一个缺点是,大多数团队将其用作未设计REST端点响应的借口。它们仅返回序列化的实体对象。
但是,如果您未实现非常简单的CRUD操作,则客户很可能会从精心设计的响应中受益。以下是基本书店应用程序的一些示例:

  • 当您返回搜索书的结果时,您可能只想返回书的标题和价格,书作者和出版商的名称以及平均客户评价。使用专门设计的JSON文档,您可以避免不必要的信息,并嵌入作者,发布者和平均评分的信息,而不用提供指向他们的链接。
  • 当客户请求有关一本书的详细信息时,响应很可能与实体的序列化表示非常相似。但是会有一些重要的差异。您的JSON文档可能包含书名,标题,其他说明以及有关这本书的其他信息。但是,有些信息您不想共享,例如批发价或该书的当前库存。您可能还希望排除与本书作者和评论的关联。

基于用例特定的DTO类创建这些不同的表示非常简单。但是,基于实体对象图进行相同操作要困难得多,并且很可能需要一些手动映射。

支持您的API的多个版本
如果您的应用程序使用了一段时间,则需要添加新的REST端点并更改现有的REST端点。如果不能总是同时更新所有客户端,这将迫使您支持API的多个版本。
在API中公开实体的同时做到这一点是一项艰巨的挑战。然后,您的实体将成为当前使用的和旧的,已过时的,使用@Transient[url=https://thoughts-on-java.org/hibernate-tips-map-1-attribute-2-columns/]注释的[/url]属性的混合,这样它们就不会持久化在数据库中。
如果要公开DTO,则支持API的多个版本要容易得多。这将持久层与API分离开来,您可以向应用程序引入迁移层。这一层将将调用从旧API映射到新API所需的所有操作分开。这使您可以提供当前API的简单有效的实现。而且,每当您停用旧API时,都可以删除迁移层。

结论
如您所见,我不喜欢在API中公开实体的原因有很多。但我也同意,它们都不会产生无法解决的问题。这就是为什么关于此主题的讨论如此之多的原因。
如果您在团队中进行讨论,您需要问自己:您是否想花费更多的精力来解决所有这些问题,以避免实体和DTO类之间非常基本的映射?
以我的经验,这是不值得的。我更喜欢将API与持久层分开,并实现一些基本的实体到DTO的映射。这使我的代码易于阅读,并且使我可以灵活地更改应用程序的所有内部部分,而不必担心任何客户端。