简体   繁体   中英

DTO conveter pattern in Spring Boot

The main question is how to convert DTOs to entities<\/strong> and entities to Dtos<\/strong> without breaking SOLID<\/a> principles.
For example we have such json:

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

Try to decouple the conversion from the other layers as much as possible:

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)
  }
}

You could even define a custom Converter<RoleEntity, String> but that may stretch the whole abstraction a bit too far.

As some other pointed out this kind of abstraction hides a part of the application that may perform very poorly when used for collections (as DB queries could normally be batched. I would advice you to define a Converter<List<UserEntity>, List<UserDto>> which may seem a little cumbersome when converting a single object but you are now able to batch your database requests instead of querying one by one - the user cannot use said converter wrong (assuming no ill intention).

Take a look at MapStruct or ModelMapper if you would like to have some more comfort when defining your converters. And last but not least give datus a shot (disclaimer: I am the author), it lets you define your mapping in a fluent way without any implicit functionality:

@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;
  }
}

(This example would still suffer from the db bottleneck when converting a Collection<UserDto>

I would argue this would be the most SOLID approach, but the given context / scenario is suffering from unextractable dependencies with performance implications which makes me think that forcing SOLID might be a bad idea here. It's a trade-off

personally, converters should be between your controllers and services, the only things DTOs should worry about is the data in your service layer and how which information to expose to your controllers.

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

in your case, you can make use of JPA to populate roles of your users at the persistence layer.

If you have a service layer, it would make more sense to use it to do the conversion or make it delegate the task to the converter.
Ideally, converters should be just converters : a mapper object, not a service.
Now if the logic is not too complex and converters are not reusable, you may mix service processing with mapping processing and in this case you could replace the Converter prefix by Service .

And also it would seem nicer if only the services communicate with the repository.
Otherwise layers become blur and the design messy : we don't know really any longer who invokes who.

I would do things in this way :

controller -> service -> converter 
                      -> repository

or a service that performs itself the conversion (it conversion is not too complex and it is not reusable) :

controller -> service ->  repository            

Now to be honest I hate DTO as these are just data duplicates.
I introduce them only as the client requirements in terms of information differ from the entity representation and that it makes really clearer to have a custom class (that in this case is not a duplicate).

I suggest that you just use Mapstruct to solve this kind of entity to dto convertion issue that you are facing. Through an annotation processor the mappings from dto to entity and vice versa are generated automatically and you just have to inject a reference from your mapper to your controller just like you normally would do with your repositories ( @Autowired ).

You can also check out this example to see if it fit your needs.

That's the way I'd likely do it. The way I'd conceptualize it is that the User converter is responsible for user / user dto conversions, and as such it rightly shouldn't be responsible for role / role dto conversion. In your case, the role repository is acting implicitly as a role converter that the user converter is delegating to. Maybe someone with more in-depth knowledge of SOLID can correct me if I'm wrong, but personally I feel like that checks out.

The one hesitation I would have, though, would be the fact that you're tying the notion of conversion to a DB operation which isn't necessarily intuitive, and I'd want to be careful that months or years into the future some developer doesn't inadvertently grab the component and use it without understanding the performance considerations (assuming you're developing on a larger project, anyways). I might consider creating some decorator class around the role repository that incorporates caching logic.

I think the way to do it cleanly is to include a Role DTO that you convert to the RoleEntity. I might use a simplified User DTO in case that it is read only. For example, in case of unprivileged access.

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

Instead of creating separate convertor clas, you can give that responsibility to Entity class itself.

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;
    }
}

Usage;

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

In this way you have your domain in one place. You can use Spring BeanUtils to set properties. You can do the same for RoleEntity and decide whether to lazy/eager load when loading UserEntity using ORM tool.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM