微服务Restful API - 是否需要使用DTO?

30

REST API - 是否需要使用DTO?

我想在微服务的环境下重新提出这个问题。以下是原问题的引用。

我正在为一个项目创建REST-API,并一直阅读有关最佳实践的文章。许多人似乎反对使用DTO,只需公开领域模型,而其他人似乎认为DTO(或用户模型或任何您想要称呼的内容)是不好的实践。就我个人而言,我认为这篇文章很有道理。

然而,我也了解到DTO的缺点,以及所有额外映射代码、可能与其DTO副本完全相同的领域模型等等。

现在,我的问题

我更倾向于在应用程序的所有层中使用一个对象(换句话说,只需公开领域对象,而不是创建DTO并手动复制每个字段)。并且,我的Rest合同与领域对象之间的差异可以使用Jackson注释进行处理,例如@JsonIgnore@JsonProperty(access = Access.WRITE_ONLY)@JsonView等。或者,如果有一个或两个字段需要进行转换,而不能使用Jackson注释来完成,则将编写自定义逻辑来处理这些字段(相信我,在我的五年多的Rest服务旅程中,我甚至没有遇到过这种情况)。

我想知道,如果不将领域对象复制到DTO中是否会有任何真正的副作用。


与链接的问题一样,这完全是基于个人观点的(尽管它确实是一个非常好的问题)。 - fps
@FedericoPeraltaSchaffner 感谢您的评论。我正在寻找更多的数据点/事实,而不是意见 :) 如果我无法从Stackoverflow平台获得这个问题的答案,那么我可能无法从其他任何地方获得它。在我看来,Stackoverflow应该重新考虑什么应该被归类为意见。同时,我明白如果我问“Gradle Vs Maven,我应该选择哪一个”,那就是寻求意见的问题。 - so-random-dude
1
是的,我同意你的观点,问题在于很难划定那条界限... - fps
1
很累,因为许多问题被关闭,因为它们是基于观点的。我喜欢意见,通常它们对我来说比事实更有价值(我可以轻松在目标库/框架等的来源/文档中找到)。这个人只是需要帮助... - Stanislav Bashkyrtsev
就我个人而言,我会投票支持Danylo Zatorsky的答案。DTO确实是面向客户端驱动的,并且是通过契约设计的。在一些微服务模式中,比如聚合,DTO将在不同的微服务之间发挥重要作用。 - Allen Shi
3个回答

30

我支持使用DTO,原因如下:

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

  • 14

    只暴露领域对象的优点

    1. 你写的代码越少,产生的错误就越少。
      • 尽管在我们的代码库中有广泛(值得商榷)的测试用例,但我还是遇到了由于错过/错误地从领域对象复制到DTO或反过来而导致的错误。
    2. 可维护性 - 较少的样板代码。
      • 如果我要添加一个新属性,我不需要在Domain、DTO、Mapper和测试用例中都添加。当然,别告诉我可以使用反射beanCopy工具来实现这一目的,这完全违背了初衷。
      • 我知道Lombok、Groovy、Kotlin可以做到,但它们只能为我节省getter setter的问题。
    3. DRY原则
    4. 性能
      • 我知道这属于“过早的性能优化是万恶之源”的范畴。但仍然可以为每个请求节省一些CPU周期,因为不必创建(并稍后垃圾回收)另一个对象(至少如此)

    缺点

    1. DTOs将为您提供长期的更灵活性。
      • 如果只有我需要那种灵活性。迄今为止,我遇到的都是使用HTTP进行的CRUD操作,可以使用一对@JsonIgnores来管理。或者如果有一两个字段需要转换而Jackson注释无法处理,正如我之前所说,我可以编写自定义逻辑来处理它们。
    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)进行的。


    1
    对于使用缺乏“领域模型”的CRUD服务,我完全同意你的观点。而对于具有丰富复杂领域模型的服务,这会稍微困难一些。因此,在我看来,这取决于具体情况,而且在微服务设置中,可能会因服务和上下文的不同而有所不同。 - Nikolaj Dam Larsen

    5
    如果你使用CQRS,那么这个决定就简单多了,因为:
    - 对于写入端,你使用的是已经是DTO的“命令”(Commands);在领域层中,富行为对象“聚合”(Aggregates)没有被暴露或查询,所以这里没有问题。 - 对于读取端,因为你使用的是一个薄层,从持久化存储中获取的对象应该已经是DTO了。不应该有映射问题,因为你可以为每个用例都有一个“读模型”(readmodel)。在最坏的情况下,你可以使用类似GraphQL的东西来选择你需要的字段。
    如果你不将读和写分开,那么这个决定就更难了,因为两种解决方案都有权衡之处。

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