web-dev-qa-db-fra.com

ModelMapper: choisissez un mappage basé sur la classe enfant

TL; DR

Je veux utiliser modelMapper d'une manière que je mappe de AbstractParent à AbstractParentDTO et plus tard dans ModelMapper-Config appeler les mappeurs spécifiques pour chaque sous-classe, puis ignorer le reste des mappages (classe abstrac).

Comment est-ce possible? Est-ce la bonne approche? Y a-t-il un défaut de conception?


Ce que j'ai:

L'entité parent:

@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "type")
public abstract class Parent {
//some more fields
}

Une entité enfant:

//Basic Lombok Annotations
@DiscriminatorValue("child_a")
public class ChildA extends Parent {
//some more fields
}

Une autre entité enfant:

@DiscriminatorValue("child_b")
public class ChildB extends Parent {
//some more fields   
}

Ensuite, j'ai la classe DTO parent:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonSubTypes({
@JsonSubTypes.Type(value = ChildA.class, name = "child_a"),
@JsonSubTypes.Type(value = ChildB.class, name = "child_b"),
public abstract class ParentDTO {
//some more fields
}

Un enfant DTO:

public class ClassADTO extends ParentDTO {
//some more fields
}

et un autre DTO:

public class ClassBDTO extends ParentDTO {
//some more fields
}

Dans mon cas, j'obtiendrai les DTO du contrôleur et les mapperai aux entités lors de leur remise au service. Je vais devoir faire la même chose dans 5-6 Endpoints.

Les points de terminaison ressemblent à peu près à ceci:

@PreAuthorize(CAN_WRITE)
@PutMapping("/{id}")
public ResponseEntity<ParentDTO> update(
        @PathVariable("id") UUID id,
        @RequestBody @Valid ParentDTO parentDTO) {

    Parent parent = parentService.update(id, parentDTO);

    if (parentDTO instanceof ChildADTO) {
        return ResponseEntity.ok(modelMapper.map(parent, ChildADTO.class));
    } else if (parentDTO instanceof ChildBDTO) {
        return ResponseEntity.ok(modelMapper.map(parent, ChildBDTO.class));
    }
    throw new BadRequestException("The Parent is not Valid");
}

Seulement, j'ai encore quelques enfants qui rendent les choses encore plus volumineuses.


Ce que je veux:

Au lieu de vérifier un tas de fois quelle instance est le DTO (ou Entité), je veux simplement écrire par exemple:

modelmapper.map(parent, ParentDTO.class)

et effectuez la vérification "instance de ..." UNE FOIS dans ma configuration ModelMapper.


Ce que j'ai essayé:

J'ai déjà différents convertisseurs pour chaque direction possible et cas de mappage définis dans ma configuration ModelMapper (car ils nécessitent de toute façon un mappage plus complexe).

J'ai essayé de résoudre mon problème en écrivant un autre convertisseur pour les classes parentes et en le définissant en tant que PreConverter ModelMapper:

    //from Entity to DTO
    Converter<Parent, ParentDTO> parentParentDTOConverter = mappingContext -> {
        Parent source = mappingContext.getSource();
        ParentDTO dest = mappingContext.getDestination();

        if (source instanceof CHildA) {
            return modelMapper.map(dest, ChildADTO.class);
        } else if (source instanceof ChildB) {
            return modelMapper.map(dest, ChildBDTO.class);
        } 
        return null;
    };

et:

modelMapper.createTypeMap(Parent.class, ParentDTO.class)
                .setPreConverter(parentParentDTOConverter);

Mais je reçois toujours la même MappingError:

1) Échec de l'instanciation de l'instance de destination com.myexample.data.dto.ParentDTO. Assurez-vous que com.myexample.data.dto.ParentDTOO a un constructeur sans argument non privé.

que je reçois (je suppose), je ne peux pas construire un objet d'une classe abstraite. Mais ce n'est pas ce que j'essaie, n'est-ce pas? Je suppose que modelMapper fait toujours le reste du mappage après avoir terminé avec mon PreConverter. J'ai également essayé de le définir avec .setConverter mais toujours avec le même résultat.


  • Quelqu'un sait-il comment "désactiver" les mappages personnalisés? Je ne veux pas vraiment écrire des "pseudo-mappeurs" qui agissent comme des mappeurs et appellent simplement les mappeurs spécifiques pour chaque scénario.

  • Ma conception est-elle simplement mauvaise? Comment l'amélioreriez-vous?

  • Est-ce que cela n'est pas encore implémenté dans ModelMapper?

Toute aide et indice est apprécié.

9
Simon

J'utiliserais ObjectMapper au lieu de ModelMapper.

Dans la classe Parent ajoutez la possibilité d'obtenir la valeur du discriminateur.

//..
public class Parent {

    @Column(name = "type", insertable = false, updatable = false)
    private String type;
    //getters and setters
}

Votre ParentDTO doit être mappé sur un enfant (*) DTO

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "type")
@JsonSubTypes({
        @JsonSubTypes.Type(value = ChildADTO.class, name = "child_a"),
        @JsonSubTypes.Type(value = ChildBDTO.class, name = "child_b")
})
 public abstract class ParentDTO {
   // ..
 }

dans le service/méthode de conversion, ajoutez un mappeur d'objets avec ignore unknown (pour ignorer ce que vous n'avez pas déclaré dans votre classe DTO)

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

appelez simplement:

Parent parent = // get from repository
ParentDTO parentDTO = objectMapper.readValue(objectMapper.writeValueAsBytes(parent), ParentDTO.class);

De cette façon, votre ParentDTO est toujours instancié avec le bon type.

4
Adina Fometescu

Eh bien, la solution que j'ai trouvée utilise des convertisseurs. Dans ce cas, modelMapper n'essaie pas de créer une nouvelle instance de classe abstraite, mais utilise directement le convertisseur.

Vous pouvez mettre tous les convertisseurs au même endroit

modelMapper.createTypeMap(ChildA.class, ParentDTO.class)
            .setConverter(mappingContext -> modelMapper.map(mappingContext.getSource(), ClassADTO.class));

modelMapper.createTypeMap(ChildB.class, ParentDTO.class)
            .setConverter(mappingContext -> modelMapper.map(mappingContext.getSource(), ClassBDTO.class));
....
3
Feedforward

Que diriez-vous

    TypeMap<Parent.class, ParentDTO.class> typeMap = modelMapper.createTypeMap(Parent.class, ParentDTO.class);

    typeMap
     .include(ChildA .class, ClassADTO .class)
    .include(ChildB.class, ClassbDTO.class);

référence: http://modelmapper.org/user-manual/type-map-inheritance

0
ahll