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