DTO不过是一种将服务器端领域模型与HTTP资源中的表现形式解耦的手段之一。你也可以使用其他解耦手段,这就是Spring Data REST所做的。
是的,Spring Data REST会检查你在服务器端拥有的领域模型,以推断其所暴露的资源的表现形式。但是,它应用了几个关键概念来缓解一个天真的领域对象暴露所带来的问题。
天真的“我将我的领域对象扔到Jackson面前”的根本问题在于,从普通实体模型出发,很难推断出合理的表现形式边界。特别是从数据库表派生的实体模型往往有将几乎所有东西连接在一起的习惯。这源于大多数持久化技术(即关系型数据库)中根本没有重要的领域概念,如聚合。
然而,我认为在这种情况下,“不要暴露你的领域模型”更多地是针对这个问题的症状而非核心。如果你正确设计你的领域模型,那么在领域模型和有效驱动该模型通过状态变化所需的好表示之间有很大的重叠。一些简单的规则:这种方法通常与DDD聚合在HTTP级别上描述的一致性保证相容。默认情况下,PUT
请求不跨越多个聚合,这是一件好事,因为它意味着资源的一致性范围匹配您域的概念。
您可以为域对象引入尽可能多的DTO。在大多数情况下,域对象中捕获的字段将以某种方式反映到表示中。我还没有看到实体Customer
包含一个firstname
,lastname
和emailAddress
属性,并且这些属性在表示中完全无关紧要。
DTO的引入绝不能保证解耦。我见过太多项目仅出于模仿原因引入了DTO,仅仅复制了支持它们的实体的所有字段,从而导致额外的工作量,因为每个新字段都必须添加到DTO中。但嘿,解耦!不。¯\_(ツ)_/¯
说到这里,当然有些情况下你可能想要略微调整这些属性的表示方式,特别是如果你使用强类型值对象来表示例如EmailAddress
(很好!),但仍然希望在JSON中将其呈现为普通的String
。但这绝不是一个问题:Spring Data REST在内部使用Jackson,它为您提供了各种各样的手段来调整表示方式——注释、混入以使注释保持在域类型之外、自定义序列化程序等。因此,在两者之间有一个映射层。这是我在2016年SpringOne平台上发表的演讲中的一张幻灯片,概括了这种情况。
完整的幻灯片可以在这里找到。此外,InfoQ上还有一份演讲录音可供使用。
Spring Data REST存在的目的是让您能够专注于下划线部分。我们绝不认为您仅通过启用Spring Data REST就能构建一个出色的API。我们只想减少样板代码的数量,以便您有更多时间思考有趣的部分。
就像Spring Data通常减少编写标准持久性操作所需的样板代码一样。没有人会争辩说您实际上可以仅通过CRUD操作构建真实的应用程序。但是,我们通过省去无聊的部分,使您更加集中地思考真正的领域挑战(而您实际上应该这样做:))。
您可以非常有选择性地覆盖某些资源以完全控制它们的行为,包括手动映射域类型到DTOs。如果需要,您还可以将自定义功能放置在Spring Data REST提供的功能旁边,并将两者连接起来。对您使用的内容要有选择性。
Order
实例,但对其处理进行了调整以引入自定义要求,并完全手动实现了付款部分。空运教派编程
,请参阅https://en.wikipedia.org/wiki/Cargo_cult_programming - Alireza FattahiSpring Data REST可以快速原型设计并基于数据库结构创建REST API。与其他编程技术相比,我们在分钟级别而非天级别内完成这些操作。
你所付出的代价是,你的REST API与数据库结构紧密耦合。有时候,这是一个大问题。有时候则不是。这主要取决于你数据库设计的质量和你改变它以适应API用户需求的能力。
简言之,我认为Spring Data REST是一种在特定情况下可以节省你大量时间的工具。而不是一种能够解决任何问题的万金油。
我们曾经在项目中为每个实体使用DTO,采用完全传统的分层方式(数据库、DTO、Repository、Service、Controllers等)。希望有一天DTO能拯救我们的生命 :)
所以对于一个简单的City
实体,它具有id,name,country,state
属性, 我们做如下操作:
City
表格,具有id,name,county,....
列CityDTO
,具有id,name,county,....
属性(与数据库完全相同)CityRepository
,具有findCity(id),....
方法CityService
,具有findCity(id) { CityRepository.findCity(id) }
方法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,在复杂的用例中,我们仍然可以采用传统的分层方式,并让Service
和Controller
做一些业务。我承认,使用动态代码(如spring-data-rest)在执行框架不支持的任何操作时会有成本。例如,我们的服务器需要支持大量的批量插入/更新操作。如果我们只需要在一个特定情况下使用自定义行为,那么不使用spring-data-rest实现它肯定更容易。事实上,这可能太容易了。这些单个案例往往会增加。随着DTO和相关代码的数量增长,不一致性最终变得非常繁琐且难以维护。在一些非动态服务器实现中,我们有数百个DTO和POJO,可能已经不再被任何东西使用。但是,由于它们的数量每个月都在增长,我们被迫继续支持它们。
使用spring-data-rest,我们早期就付出了定制的代价。对于我们的多层硬编码实现,我们稍后付出了代价。哪种更受欢迎取决于许多因素(包括团队的知识和项目的预期寿命)。两种类型的项目都可能因其自身的重量而崩溃。但是,随着时间的推移,我对更具动态性的实现(如没有DTO的spring-data-rest)变得更加舒适。特别是当项目缺乏良好的规范时,随着时间的推移,这样的项目很容易在其大量样板代码中淹没不一致性。