Spring Boot 中的 DTO 转换器模式

Posted

技术标签:

【中文标题】Spring Boot 中的 DTO 转换器模式【英文标题】:DTO conveter pattern in Spring Boot 【发布时间】:2018-06-01 20:24:10 【问题描述】:

主要问题是如何在不违反SOLID 原则的情况下将DTO 转换为实体实体转换为Dto。 比如我们有这样的json:

 id: 1,
  name: "user", 
  role: "manager" 
 

DTO 是:

public class UserDto 
 private Long id;
 private String name;
 private String roleName;

实体是:

public class UserEntity 
  private Long id;
  private String name;
  private Role role
 
public class RoleEntity 
  private Long id;
  private String roleName;

还有有用的Java 8 DTO conveter pattern。

但在他们的示例中,没有 OneToMany 关系。为了创建 UserEntity,我需要使用 dao 层(服务层)通过 roleName 获取角色。我可以将 UserRepository(或 UserService)注入转换程序吗?因为看起来转换器组件会破坏SRP,所以它必须只转换,不能知道服务或存储库。

转换器示例:

@Component
public class UserConverter implements Converter<UserEntity, UserDto> 
   @Autowired
   private RoleRepository roleRepository;    

   @Override
   public UserEntity createFrom(final UserDto dto) 
       UserEntity userEntity = new UserEntity();
       Role role = roleRepository.findByRoleName(dto.getRoleName());
       userEntity.setName(dto.getName());
       userEntity.setRole(role);
       return userEntity;
   

   ....

在conveter类中使用repository好不好?或者我应该创建另一个服务/组件来负责从 DTO 创建实体(如 UserFactory)?

【问题讨论】:

我觉得repository的使用是正确的。从 DTO 转换为 DO 时,我一直在我的应用程序中执行此操作,并且我不知道任何其他绕过查询的方法。 几天前我问了一个非常相似的问题。也许这可以帮助你***.com/questions/47843039/… 转换器不应依赖于其他逻辑,尤其是数据库逻辑。如果您选择后者,测试会容易得多,因为您可以传递您想要的角色,而无需像第一种方法那样注入存储库。 我认为 domain 不应该负责 DTO 转移。它不知道来自外部的 DTO。基础设施层应将 DTO 映射到域 (VO) 【参考方案1】:

如果您有服务层,使用它来进行转换或将任务委托给转换器会更有意义。 理想情况下,转换器应该只是转换器:映射器对象,而不是服务。 现在,如果逻辑不太复杂且转换器不可重用,您可以将服务处理与映射处理混合使用,在这种情况下,您可以将 Converter 前缀替换为 Service

而且,如果只有服务与存储库进行通信,那看起来会更好。 否则图层会变得模糊,设计也很混乱:我们真的不知道谁调用了谁。

我会以这种方式做事:

controller -> service -> converter 
                      -> repository

或者一个自己执行转换的服务(它的转换不是太复杂并且不可重用):

controller -> service ->  repository            

老实说,我讨厌 DTO,因为这些只是数据重复。 我介绍它们只是因为客户在信息方面的要求与实体表示不同,并且拥有一个自定义类(在这种情况下不是重复的)确实更清楚。

【讨论】:

DTO 是必不可少的,因为它们提供了与客户端的隔离层。我只是在将 webapp 与没有服务或 api 层的服务分离的过程中。很痛苦 @tomaytotomato 我想是的。在某些情况下,将模型与 DTO 解耦非常有意义,因此 DTO 甚至是非常可取的。但是,我认为在您不需要它们时通过预期尽早创建 DTO 是一种开销。在不需要时处理/操作/单元测试一个愚蠢的数据抽象也是非常痛苦的。 DTO 应该留在 Web 层,因此只知道 @Controller 转换器 @Component 【参考方案2】:

您可以将该责任交给实体类本身,而不是创建单独的转换器类。

public class UserEntity 
    // properties

    public static UserEntity valueOf(UserDTO userDTO) 
        UserEntity userEntity = new UserEntity();
        // set values;
        return userEntity;
    

    public UserDTO toDto() 
        UserDTO userDTO = new UserDTO();
        // set values
        return userDTO;
    

用法;

UserEntity userEntity = UserEntity.valueOf(userDTO);
UserDTO userDTO = userEntity.toDto();

通过这种方式,您可以将您的域放在一个地方。您可以使用 Spring BeanUtils 来设置属性。 您可以对 RoleEntity 执行相同的操作,并在使用 ORM 工具加载 UserEntity 时决定是否延迟/急切加载。

【讨论】:

我认为这不是将实体绑定到单个 DTO 的好方法。根据使用数据的上下文,可以有许多 DTO。 我认为 Entity 的唯一职责是将字段映射到数据库表,仅此而已。因此,它应该只包含 JPA 映射。对于业务逻辑,您可以引入一个域对象,用于传输数据 - DTO。【参考方案3】:

我建议您只使用Mapstruct 来解决您面临的这种实体到dto 转换的问题。通过注释处理器,自动生成从 dto 到实体的映射,反之亦然,您只需将映射器的引用注入到控制器,就像您通常对存储库所做的那样 (@Autowired)。

您也可以查看this 示例,看看它是否符合您的需求。

【讨论】:

【参考方案4】:

这就是我可能会这样做的方式。我将其概念化的方式是用户转换器负责用户/用户 dto 转换,因此它不应该负责角色/角色 dto 转换。在您的情况下,角色存储库隐含地充当用户转换器委派的角色转换器。如果我错了,也许对 SOLID 有更深入了解的人可以纠正我,但我个人觉得这样可以检验。

不过,我会犹豫的是,您将转换的概念与不一定直观的数据库操作联系起来,而且我希望在几个月或几年内小心未来,一些开发人员不会在不了解性能考虑的情况下无意中抓取组件并使用它(假设您正在开发一个更大的项目,无论如何)。我可能会考虑围绕角色存储库创建一些包含缓存逻辑的装饰器类。

【讨论】:

【参考方案5】:

尽量将转换与其他层解耦:

public class UserConverter implements Converter<UserEntity, UserDto> 
   private final Function<String, RoleEntity> roleResolver;

   @Override
   public UserEntity createFrom(final UserDto dto) 
       UserEntity userEntity = new UserEntity();
       Role role = roleResolver.apply(dto.getRoleName());
       userEntity.setName(dto.getName());
       userEntity.setRole(role);
       return userEntity;
  


@Configuration
class MyConverterConfiguration 
  @Bean
  public Converter<UserEntity, UserDto> userEntityConverter(
               @Autowired RoleRepository roleRepository
  ) 
    return new UserConverter(roleRepository::findByRoleName)
  

您甚至可以定义一个自定义Converter&lt;RoleEntity, String&gt;,但这可能会使整个抽象有点过分。

正如其他一些人指出的那样,这种抽象隐藏了应用程序的一部分,该部分在用于集合时可能性能很差(因为 DB 查询通常可以批量处理。我建议您定义一个 Converter&lt;List&lt;UserEntity&gt;, List&lt;UserDto&gt;&gt;,这可能看起来像转换单个对象时有点麻烦,但您现在可以批量处理数据库请求,而不是逐个查询 - 用户不能错误地使用所述转换器(假设没有恶意)。

查看MapStruct 或ModelMapper,如果您希望在定义转换器时更加舒适。最后但并非最不重要的一点是给datus 一个机会(免责声明:我是作者),它可以让您以流畅的方式定义您的映射,而无需任何隐式功能:

@Configuration
class MyConverterConfiguration 

  @Bean
  public Mapper<UserDto, UserEntity> userDtoCnoverter(@Autowired RoleRepository roleRepository) 
      Mapper<UserDto, UserEntity> mapper = Datus.forTypes(UserDto.class, UserEntity.class)
        .mutable(UserEntity::new)
        .from(UserDto::getName).into(UserEntity::setName)
        .from(UserDto::getRole).map(roleRepository::findByRoleName).into(UserEntity::setRole)
        .build();
      return mapper;
  

(这个例子在转换Collection&lt;UserDto&gt;时仍然会遇到数据库瓶颈

我认为这将是最可靠的方法,但是给定的上下文/场景受到无法提取的依赖性以及性能影响,这让我认为在这里强制使用 SOLID 可能是个坏主意。这是一个权衡

【讨论】:

【参考方案6】:

就个人而言,转换器应该位于控制器和服务之间,DTO 唯一应该担心的是服务层中的数据以及如何向控制器公开哪些信息。

controllers <-> converters <-> services ... 

在您的情况下,您可以使用 JPA 在持久层填充用户的角色。

【讨论】:

【参考方案7】:

我认为干净利落的方法是包含一个您转换为 RoleEntity 的 Role DTO。如果它是只读的,我可能会使用简化的用户 DTO。例如,在非特权访问的情况下。

扩展你的例子

public class UserDto 
 private Long id;
 private String name;
 private RoleDto role;

角色 DTO 为

public class RoleDto 
  private Long id;
  private String roleName;

还有 JSON

 
  id: 1,
  name: "user", 
  role: 
   id: 123,
   roleName: "manager"

然后您可以在转换 UserConverter 中的用户时将 RoleDto 转换为 RoleEntity 并删除存储库访问权限。

【讨论】:

以上是关于Spring Boot 中的 DTO 转换器模式的主要内容,如果未能解决你的问题,请参考以下文章

如何在Spring Boot Rest API中的BeanUtils.copyProperties中将String转换为枚举

Spring Boot,决定为 REST 和 JPA 分别创建 DTO 对象

Spring Boot MapStruct

事务中的 Spring Boot LazyInitializationException

Spring Boot 查询 DTO

如何在 Spring Boot 中从双向表关系生成 DTO