DTO conveter pattern in Spring Boot
Asked Answered
E

7

13

The main question is how to convert DTOs to entities and entities to Dtos without breaking SOLID principles.
For example we have such json:

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

DTO is:

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

And entities are:

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

And there is usefull Java 8 DTO conveter pattern.

But in their example there is no OneToMany relations. In order to create UserEntity I need get Role by roleName using dao layer (service layer). Can I inject UserRepository (or UserService) into conveter. Because it seems that converter component will break SRP, it must convert only, must not know about services or repositories.

Converter example:

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

   ....

Is it good to use repository in the conveter class? Or should I create another service/component that will be responsible for creating entities from DTOs (like UserFactory)?

Exotic answered 19/12, 2017 at 11:47 Comment(4)
I think the use of the repository is correct. I do this all the time in my application when converting from DTO to DO and I don't know any other way that bypasses the query.Moina
I asked a quite similar question some days ago. Maybe this can help you #47843539Rna
Converter shouldn't be dependent on other logic especially DB logic. If you choose latter, it will be lot easier to test, as in you can pass roles you desire without injecting repository as in first approach.Mongolian
I think domain should not be responsible for DTO transfer. It does not know about DTO coming from outside. Infrastructure layer should map DTO into domain (VO)Photocompose
B
7

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

Bellarmine answered 4/6, 2019 at 20:9 Comment(0)
B
3

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

Bethink answered 19/12, 2017 at 12:12 Comment(3)
DTOs are a necessary evil as they provide a layer of separation from the client. I am just in the process of decoupling a webapp from a service that has no service or api layer. It is very painfulVesuvian
@Vesuvian I suppose that it is. In some cases, decoupling the model with DTOs makes really sense and so DTO is even very desirable. But creating DTO early by anticipation while you don't need them is I think an overhead. Handling/Manipulating/Unit testing a dumb data abstraction when you don't need it is also very painful.Bethink
DTOs should stay in the web tier, thus be known of the @Controller a converter @Component onlyHives
C
3

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.

Ceroplastics answered 5/6, 2019 at 20:19 Comment(0)
A
1

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.

Aiguillette answered 3/6, 2019 at 13:38 Comment(0)
H
1

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.

Harri answered 3/6, 2019 at 23:37 Comment(0)
S
1

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.

To expand your example

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

with the Role DTO as

public class RoleDto {
  private Long id;
  private String roleName;
}

And the JSON

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

Then you can convert the RoleDto to RoleEntity while converting the User in your UserConverter and remove the repository access.

Succinct answered 4/2, 2022 at 18:0 Comment(0)
S
-2

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.

Scrabble answered 2/6, 2019 at 9:36 Comment(2)
I don't think this is a good approach binding the Entity to a single DTO. There can be many DTOs depending on the context where the data is used.Autoeroticism
I think the only responsibility of Entity is to map fields to database table, nothing else. Hence, it should only contain JPA mappings. For business logic you can introduce a domain object, for transferring data - a DTO.Brasserie

© 2022 - 2024 — McMap. All rights reserved.