微服务Restful API - DTO或不?

so-*_*ude 17 java spring web-services spring-mvc spring-boot

REST API - DTO或不是?

我想在微服务的背景下重新提出这个问题.这是原始问题的引用.

我目前正在为一个项目创建一个REST-API,并且正在阅读关于最佳实践的文章.许多人似乎反对DTO,只是暴露域模型,而其他人似乎认为DTO(或用户模型或任何你想称之为的)是不好的做法.就个人而言,我认为这篇文章很有意义.

但是,我也理解DTO的缺点,包括所有额外的映射代码,可能与其DTO对应物100%相同的域模型等等.

现在,我的问题

我更倾向于在我的应用程序的所有层中使用一个Object(换句话说,只是暴露域对象而不是创建DTO并手动复制每个字段).和VS代码在我的合同中的差异可以用杰克逊注释等处理@JsonIgnore@JsonProperty(access = Access.WRITE_ONLY)@JsonView等).或者,如果有一个或两个字段需要使用Jackson Annotation无法完成的转​​换,那么我将编写自定义逻辑来处理这个问题(相信我,我甚至没有遇到过这种情况,甚至在我5年多的时间里也没有休息服务的长途旅行)

我想知道我是否遗漏了没有将域复制到DTO的任何真正的不良影响

Dan*_*sky 20

我会投票使用DTO,这就是为什么:

  • 不同的请求(事件)和您的数据库实体.通常情况下,您的请求/响应与您在域模型中的请求/响应不同.特别是在微服务架构中,它有很多来自其他微服务的事件.例如,您有Order实体,但是从另一个微服务获得的事件是OrderItemAdded.即使一半的事件(或请求)与实体相同,但为了避免混乱,为所有事件设置DTO仍然有意义.
  • 在您公开的DB模式和API之间进行耦合.使用实体时,您基本上会揭示您在特定微服务中建模数据库的方式.在MySQL中,您可能希望让您的实体拥有关系,它们在组合方面将非常庞大.在其他类型的DB中,您将拥有没有大量内部对象的扁平实体.这意味着如果您使用实体来公开您的API并希望将您的数据库从MySQL改为Cassandra - 那么您也需要更改您的API,这显然是一件坏事.
  • 消费者驱动的合同.可能这与之前的子弹相关,但是DTO使得更容易确保微服务之间的通信在其演变过程中不被破坏.由于合同和数据库没有耦合,因此更容易测试.
  • 聚合.有时您需要返回的数量超过单个数据库实体中的数量.在这种情况下,您的DTO将只是一个聚合器.
  • 表现.微服务意味着通过网络传输大量数据,这可能会使您遇到性能问题.如果您的微服务客户端需要的数据少于存储在数据库中的数据 - 您应该为它们提供更少的数据.再次 - 只需制作DTO,您的网络负载就会减少.
  • 忘掉LazyInitializationException.与您的ORM管理的域实体相比,DTO没有任何延迟加载和代理.
  • 使用正确的工具并不难以支持DTO层.通常,将实体映射到DTO和向后时会出现问题 - 每次要进行转换时都需要手动设置正确的字段.在向实体和DTO添加新字段时,很容易忘记设置映射,但幸运的是,有很多工具可以为您完成此任务.例如,我们曾经在项目中使用MapStruct - 它可以在编译时自动为您生成转换.


so-*_*ude 11

仅公开领域对象的优点

  1. 你写的代码越少,你产生的错误就越少。
    • 尽管在我们的代码库中有大量(有争议的)测试用例,但我遇到了由于从域到 DTO 或反之亦然的字段的遗漏/错误复制而导致的错误。
  2. 可维护性 - 更少的样板代码。
    • 如果我必须添加一个新属性,我当然不必添加域、DTO、Mapper 和测试用例。不要告诉我这可以使用反射 beanCopy utils 来实现,它违背了整个目的。
    • Lombok、Groovy、Kotlin 我知道,但它只会让我省去 getter setter 的麻烦。
  3. 干燥
  4. 表现
    • 我知道这属于“过早的性能优化是万恶之源”的范畴。但这仍然会节省一些 CPU 周期,因为不必为每个请求创建(以及以后的垃圾收集)一个对象(至少)

缺点

  1. 从长远来看,DTO 将为您提供更大的灵活性
    • 如果我需要那种灵活性就好了。至少,到目前为止我遇到的都是通过 http 的 CRUD 操作,我可以使用几个 @JsonIgnores 来管理它。或者,如果有一两个字段需要使用 Jackson Annotation 无法完成的转​​换,正如我之前所说,我可以编写自定义逻辑来处理这个问题。
  2. 域对象因注解而变得臃肿。
    • 这是一个合理的担忧。如果我使用 JPA 或 MyBatis 作为我的持久化框架,域对象可能有那些注解,那么也会有 Jackson 注解。就我而言,这不太适用,但我使用的是 Spring Boot,并且可以通过使用应用程序范围的属性(例如mybatis.configuration.map-underscore-to-camel-case: truespring.jackson.property-naming-strategy: SNAKE_CASE

短篇小说,至少在我的情况下,缺点并没有超过优点,因此通过将新的 POJO 作为 DTO 来重复自己没有任何意义。更少的代码,更少的错误机会。因此,继续公开域对象而不是拥有单独的“视图”对象。

免责声明:这可能适用于您的用例,也可能不适用。这个观察是根据我的用例(基本上是一个具有 15 个端点的 CRUD api)


Con*_*enu 5

如果您使用 CQRS,这个决定会简单得多,因为:

  • 对于你使用Commands的已经是 DTO的写端;Aggregates- 域层中的丰富行为对象 - 没有公开/查询,所以那里没有问题。
  • 对于读取端,因为您使用了一个薄层,所以从持久化中获取的对象应该已经是 DTO。应该没有映射问题,因为您可以readmodel为每个用例创建一个。在最坏的情况下,您可以使用 GraphQL 之类的东西来仅选择您需要的字段。

如果您不将读取与写入分开,那么决策将更加困难,因为这两种解决方案都需要权衡。