Spring Data REST暴露实体对象作为REST资源而不使用DTO是否存在问题?

46
根据我的有限经验,我反复听说过不应将实体传递到前端或通过 REST 传递,而是要使用 DTO。但是 Spring Data Rest 难道不正是这样做的吗?我简单地研究了一下投影,但那些似乎只是限制返回的数据,而且仍然期望将实体作为参数传递给保存到数据库的 post 方法。我是否漏掉了什么,或者我(和我的同事)错误地认为永远不应该传递实体?

1
你是对的,Spring Data REST 将实体暴露给前端是一个很大的错误。我不建议这样做,应该使用 DTO。问候。 - duardito
11
@duardito,您介意详细说明为什么这是一个错误,特别是在我下面给出的答案的背景下吗?使用DTO会带来什么好处? - Oliver Drotbohm
在创建上下文时,如果使用JaxB注释,则会进入Hibernate库并导致性能问题,每次调用实体时都会创建jaxb上下文。我相信在对象映射时,Jackson也会遇到同样的问题。 - Asad Abdin
5个回答

71

简而言之

DTO不过是一种将服务器端领域模型与HTTP资源中的表现形式解耦的手段之一。你也可以使用其他解耦手段,这就是Spring Data REST所做的。

详细信息

是的,Spring Data REST会检查你在服务器端拥有的领域模型,以推断其所暴露的资源的表现形式。但是,它应用了几个关键概念来缓解一个天真的领域对象暴露所带来的问题。

Spring Data REST寻找聚合,并默认相应地塑造表现形式。

天真的“我将我的领域对象扔到Jackson面前”的根本问题在于,从普通实体模型出发,很难推断出合理的表现形式边界。特别是从数据库表派生的实体模型往往有将几乎所有东西连接在一起的习惯。这源于大多数持久化技术(即关系型数据库)中根本没有重要的领域概念,如聚合。

然而,我认为在这种情况下,“不要暴露你的领域模型”更多地是针对这个问题的症状而非核心。如果你正确设计你的领域模型,那么在领域模型和有效驱动该模型通过状态变化所需的好表示之间有很大的重叠。一些简单的规则:
- 对于与另一个实体的关系,请问自己:这是否可以是一个id引用?通过使用对象引用,您将其他关系方面的许多语义拉到了您的实体中。如果做错了,通常会导致实体引用实体引用实体,这是一个更深层次的问题。在表示层面上,这使您能够切断数据、满足一致性范围等。 - 避免双向关系,因为它们在更新方面极难正确处理。
Spring Data REST 实际上采取了许多措施,将这些实体关系转换为 HTTP 级别的适当机制:链接一般以及更重要的是链接到专门管理这些关系的资源。它通过检查为实体声明的存储库来实现这一点,并基本上用一个关联资源的链接替换了相关实体的必要内联,这样您就可以明确地管理该关系。

这种方法通常与DDD聚合在HTTP级别上描述的一致性保证相容。默认情况下,PUT请求不跨越多个聚合,这是一件好事,因为它意味着资源的一致性范围匹配您域的概念。

如果DTO只是复制域对象的字段,那么强制用户使用DTO就没有意义。

您可以为域对象引入尽可能多的DTO。在大多数情况下,域对象中捕获的字段将以某种方式反映到表示中。我还没有看到实体Customer包含一个firstnamelastnameemailAddress属性,并且这些属性在表示中完全无关紧要。

DTO的引入绝不能保证解耦。我见过太多项目仅出于模仿原因引入了DTO,仅仅复制了支持它们的实体的所有字段,从而导致额外的工作量,因为每个新字段都必须添加到DTO中。但嘿,解耦!不。¯\_(ツ)_/¯

说到这里,当然有些情况下你可能想要略微调整这些属性的表示方式,特别是如果你使用强类型值对象来表示例如EmailAddress(很好!),但仍然希望在JSON中将其呈现为普通的String。但这绝不是一个问题:Spring Data REST在内部使用Jackson,它为您提供了各种各样的手段来调整表示方式——注释、混入以使注释保持在域类型之外、自定义序列化程序等。因此,在两者之间有一个映射层。
默认情况下不使用DTO本身并不是一件坏事。想象一下,如果我们要求为每个东西编写DTO,用户们会对所需的样板代码数量大声抗议!DTO只是实现目标的“一种”方法。如果可以用不同的方式实现该目标(通常可以),为什么要坚持使用DTO?

如果Spring Data REST不符合您的要求,请不要使用它。

继续定制工作,值得注意的是,Spring Data REST的存在正是为了涵盖API的那些遵循基本REST API实现模式的部分,并且该功能已经就位,以便让您有更多时间思考。
  1. 如何塑造您的领域模型
  2. 哪些部分的API最好通过超媒体驱动的交互来表达。

这是我在2016年SpringOne平台上发表的演讲中的一张幻灯片,概括了这种情况。

What makes up a REST API built with Spring Data REST

完整的幻灯片可以在这里找到。此外,InfoQ上还有一份演讲录音可供使用。

Spring Data REST存在的目的是让您能够专注于下划线部分。我们绝不认为您仅通过启用Spring Data REST就能构建一个出色的API。我们只想减少样板代码的数量,以便您有更多时间思考有趣的部分。

就像Spring Data通常减少编写标准持久性操作所需的样板代码一样。没有人会争辩说您实际上可以仅通过CRUD操作构建真实的应用程序。但是,我们通过省去无聊的部分,使您更加集中地思考真正的领域挑战(而您实际上应该这样做:))。

您可以非常有选择性地覆盖某些资源以完全控制它们的行为,包括手动映射域类型到DTOs。如果需要,您还可以将自定义功能放置在Spring Data REST提供的功能旁边,并将两者连接起来。对您使用的内容要有选择性。

示例

你可以在Spring RESTBucks中找到一个稍微高级的示例,这是RESTful Web Services书中RESTBucks示例的基于Spring(Data REST)的实现。它使用Spring Data REST来管理Order实例,但对其处理进行了调整以引入自定义要求,并完全手动实现了付款部分。

1
谢谢。还了解了什么是空运教派编程,请参阅https://en.wikipedia.org/wiki/Cargo_cult_programming - Alireza Fattahi
Spring Data REST的核心功能是为Spring Data存储库导出资源。如果API由从另一个API、缓存或数据存储中获取的数据组成,而Spring Data不支持,则需要使用Spring HATEOAS和自定义资源表示(即DTO层)。“强制用户使用DTO没有意义”,但需要注意更改实体模型可能会破坏API契约。SpringDR可以使用DTO抽象吗? - Zubin Kavarana
阅读完这篇文章后,我可以想象我的大多数旧项目都可以使用Spring Data Rest。即使你不喜欢HAL规范,也可以大部分剪裁一些部分,因为在SDR中响应结构是高度可定制的。感谢详尽的解释! - Yavuz Tas

13

Spring Data REST可以快速原型设计并基于数据库结构创建REST API。与其他编程技术相比,我们在分钟级别而非天级别内完成这些操作。

你所付出的代价是,你的REST API与数据库结构紧密耦合。有时候,这是一个大问题。有时候则不是。这主要取决于你数据库设计的质量和你改变它以适应API用户需求的能力。

简言之,我认为Spring Data REST是一种在特定情况下可以节省你大量时间的工具。而不是一种能够解决任何问题的万金油。


6

我们曾经在项目中为每个实体使用DTO,采用完全传统的分层方式(数据库、DTO、Repository、Service、Controllers等)。希望有一天DTO能拯救我们的生命 :)

所以对于一个简单的City实体,它具有id,name,country,state属性, 我们做如下操作:

  1. City表格,具有id,name,county,....
  2. CityDTO,具有id,name,county,....属性(与数据库完全相同)
  3. CityRepository,具有findCity(id),....方法
  4. CityService,具有findCity(id) { CityRepository.findCity(id) }方法
  5. CityController,具有findCity(id) { ConvertToJson( CityService.findCity(id)) }方法

为了向客户端公开城市信息,存在太多样板代码。由于这是一个简单的实体,所有层次都没有进行任何业务操作,只是对象被传递。

更改City实体从数据库开始,需要更改所有层次。(例如添加location属性,因为最终应该将 location 属性作为json公开给用户)。添加一个findByNameAndCountryAllIgnoringCase方法需要更改所有层次(每个层次都需要有新的方法)。

考虑到Spring Data Rest(当然需要使用Spring Data),这已经超出了简单的范畴!

public interface CityRepository extends CRUDRepository<City, Long> {
   City findByNameAndCountryAllIgnoringCase(String name, String country);
}
city实体最少的代码就可以向客户端公开,而且您仍然可以控制如何公开city验证安全性对象映射 ...全部都在那里。 因此,您可以调整每一件事情。
例如,如果我想保持客户端不知道city实体属性名称的更改(层分离),那么我可以使用自定义对象映射器,其在https://docs.spring.io/spring-data/rest/docs/3.0.2.RELEASE/reference/html/#customizing-sdr.custom-jackson-deserialization中进行了提及。 总结 我们尽可能多地使用Spring Data Rest,在复杂的用例中,我们仍然可以采用传统的分层方式,并让ServiceController做一些业务。

0
一个客户端/服务器发布将至少发布两个工件。这已经将客户端与服务器解耦。当服务器的API更改时,应用程序不会立即更改。即使应用程序直接使用JSON,它们仍然继续使用旧版API。
因此,解耦已经存在。重要的是要考虑服务器API在发布后可能演变的各种方式。
我主要使用DTO和许多严格的样板层与服务器SQL和消费应用程序之间进行交互的项目。这些应用程序中也同样可能存在严格的耦合。通常,在数据库模式中更改任何内容都需要我们实现一组新的端点。然后,我们支持两组端点以及每个层中的相应样板(客户端、DTO、POJO、DTO <-> POJO转换、控制器、服务、存储库、DAO、JDBC <-> POJO转换和SQL)。

我承认,使用动态代码(如spring-data-rest)在执行框架不支持的任何操作时会有成本。例如,我们的服务器需要支持大量的批量插入/更新操作。如果我们只需要在一个特定情况下使用自定义行为,那么不使用spring-data-rest实现它肯定更容易。事实上,这可能太容易了。这些单个案例往往会增加。随着DTO和相关代码的数量增长,不一致性最终变得非常繁琐且难以维护。在一些非动态服务器实现中,我们有数百个DTO和POJO,可能已经不再被任何东西使用。但是,由于它们的数量每个月都在增长,我们被迫继续支持它们。

使用spring-data-rest,我们早期就付出了定制的代价。对于我们的多层硬编码实现,我们稍后付出了代价。哪种更受欢迎取决于许多因素(包括团队的知识和项目的预期寿命)。两种类型的项目都可能因其自身的重量而崩溃。但是,随着时间的推移,我对更具动态性的实现(如没有DTO的spring-data-rest)变得更加舒适。特别是当项目缺乏良好的规范时,随着时间的推移,这样的项目很容易在其大量样板代码中淹没不一致性。


-1
从Spring文档中,我没有看到Spring Data REST公开实体,是你在这样做。Spring Data项目旨在简化访问不同数据源的过程,但是您可以决定在Spring Data Rest上公开哪个层。
重新组织您的项目将有助于解决您的问题。
您使用Spring data创建的每个@Repository更多地代表设计意义上的DAO而不是Repository。每个都与您想要访问的特定数据源紧密耦合。例如JPA、Mongo、Redis、Cassandra等。这些层旨在返回实体表示或投影。
但是,如果您从设计角度查看存储库模式,则应该具有比这些特定DAO更高的抽象层,其中您的应用程序使用这些DAO从尽可能多的不同来源获取信息,并为您的应用程序构建业务特定对象(这些对象可能更像DTO)。这可能是您想在Spring Data Rest上公开的层。
注意:我看到有人建议仅返回实体实例,因为它们具有DTO相同的属性。这通常是一种不好的做法,特别是在Spring和许多其他框架中,因为它们不会返回实际类,而是返回代理包装器,以便它们可以执行某些魔术,如延迟加载值等。

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