web-dev-qa-db-fra.com

Modèle de convertisseur DTO dans Spring Boot

La question principale est de savoir comment convertir les DTO en entités et les entités en Dtos sans casser [[# #]] solides [~ # ~] principes.
Par exemple, nous avons un tel json:

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

Le DTO c'est:

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

Et les entités sont:

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

Et il est utile modèle de convoyeur Java 8 DTO .

Mais dans leur exemple, il n'y a pas de relations OneToMany. Afin de créer UserEntity, j'ai besoin d'obtenir le rôle par roleName en utilisant la couche dao (couche de service). Puis-je injecter UserRepository (ou UserService) dans le convéter. Parce qu'il semble que le composant convertisseur se cassera SRP , il doit uniquement convertir, ne doit pas connaître les services ou les référentiels.

Exemple de convertisseur:

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

   ....

Est-il bon d'utiliser le référentiel dans la classe conveter? Ou dois-je créer un autre service/composant qui sera responsable de la création d'entités à partir des DTO (comme UserFactory)?

10
Maksym Pecheniuk

Essayez de découpler la conversion des autres couches autant que 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)
  }
}

Vous pouvez même définir un Converter<RoleEntity, String> Personnalisé, mais cela peut étirer un peu trop l'abstraction.

Comme certains l'ont souligné, ce type d'abstraction cache une partie de l'application qui peut très mal fonctionner lorsqu'elle est utilisée pour des collections (car les requêtes DB peuvent normalement être groupées. Je vous conseille de définir un Converter<List<UserEntity>, List<UserDto>> Qui peut sembler un peu encombrant lors de la conversion d'un seul objet mais vous pouvez désormais regrouper vos requêtes de base de données au lieu de les interroger une par une - l'utilisateur ne peut pas utiliser ce convertisseur de manière incorrecte (en supposant qu'aucune mauvaise intention).

Jetez un œil à MapStruct ou ModelMapper si vous souhaitez avoir plus de confort lors de la définition de vos convertisseurs. Et enfin et surtout, donnez datus une photo (avertissement: je suis l'auteur), il vous permet de définir votre cartographie de manière fluide sans aucune fonctionnalité implicite:

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

(Cet exemple souffrirait toujours du goulot d'étranglement de la base de données lors de la conversion d'un Collection<UserDto>

Je dirais que ce serait la plus SOLID approche, mais le contexte/scénario donné souffre de dépendances inextractables avec des implications de performance qui me font penser que forcer SOLID c'est peut-être une mauvaise idée ici. C'est un compromis

3
roookeee

Si vous avez une couche de service, il serait plus judicieux de l'utiliser pour effectuer la conversion ou de déléguer la tâche au convertisseur.
Idéalement, les convertisseurs ne devraient être que des convertisseurs: un objet mappeur, pas un service.
Maintenant, si la logique n'est pas trop complexe et que les convertisseurs ne sont pas réutilisables, vous pouvez mélanger le traitement de service avec le traitement de mappage et dans ce cas, vous pouvez remplacer le préfixe Converter par Service.

Et il semblerait également plus agréable que seuls les services communiquent avec le référentiel.
Sinon, les calques deviennent flous et le design en désordre: nous ne savons plus vraiment qui invoque qui.

Je ferais les choses de cette façon:

controller -> service -> converter 
                      -> repository

ou un service qui effectue lui-même la conversion (sa conversion n'est pas trop complexe et elle n'est pas réutilisable):

controller -> service ->  repository            

Pour être honnête, je déteste le DTO car ce ne sont que des doublons de données.
Je ne les présente que parce que les exigences du client en termes d'informations diffèrent de la représentation de l'entité et qu'il est vraiment plus clair d'avoir une classe personnalisée (qui dans ce cas n'est pas un doublon).

3
davidxxx

personnellement, les convertisseurs doivent être entre vos contrôleurs et vos services, les DTO ne doivent se soucier que des données de votre couche de service et de la manière dont les informations à exposer à vos contrôleurs.

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

dans votre cas, vous pouvez utiliser JPA pour remplir les rôles de vos utilisateurs au niveau de la couche de persistance.

3
stacker

C'est comme ça que je le ferais probablement. La façon dont je le conçois est que le convertisseur d'utilisateur est responsable des conversions dto utilisateur/utilisateur, et en tant que tel, il ne devrait pas être responsable à juste titre de la conversion rôle/rôle dto. Dans votre cas, le référentiel de rôles agit implicitement comme un convertisseur de rôles auquel le convertisseur d'utilisateurs délègue. Peut-être que quelqu'un avec une connaissance plus approfondie de SOLID peut me corriger si je me trompe, mais personnellement, je pense que cela vérifie.

La seule hésitation que j'aurais, cependant, serait le fait que vous liez la notion de conversion à une opération de base de données qui n'est pas nécessairement intuitive, et je voudrais être prudent pendant des mois ou des années dans le futur un développeur ne saisit pas par inadvertance le composant et ne l'utilise pas sans comprendre les considérations de performances (en supposant que vous développiez sur un projet plus grand, de toute façon). Je pourrais envisager de créer une classe décoratrice autour du référentiel de rôles qui intègre la logique de mise en cache.

1
TyrusB

Je suggère que vous utilisiez juste Mapstruct pour résoudre ce type d'entité au problème de conversion dto que vous rencontrez. Grâce à un processeur d'annotation, les mappages de dto à l'entité et vice versa sont générés automatiquement et il vous suffit d'injecter une référence de votre mappeur à votre contrôleur, comme vous le feriez normalement avec vos référentiels (@Autowired).

Vous pouvez également consulter l'exemple this pour voir s'il correspond à vos besoins.

1
gkanellis

Au lieu de créer des classes de conversion distinctes, vous pouvez confier cette responsabilité à la classe Entity elle-même.

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();

De cette façon, vous avez votre domaine en un seul endroit. Vous pouvez utiliser Spring BeanUtils pour définir les propriétés. Vous pouvez faire de même pour RoleEntity et décider de charger paresseusement/ardemment lors du chargement de UserEntity à l'aide de l'outil ORM.

0
jonarya