Comment mapper les parents et les enfants à l'aide de Mapstruct dans Spring Boot?

Dec 01 2020

J'ai un parent (produit) et des enfants (livre, mobilier) et j'aimerais mapper l'entité du produit au DTO du produit. Comme vous pouvez le voir, le produit est mappé et stocké dans une seule table dans la base de données. Comment puis-je mapper le parent, le produit, qui a des détails supplémentaires sur son enfant?

J'ai regardé ceci , ceci et cela pour avoir une idée mais pas de chance

Entité

@Entity
@Table(name = "product")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Product {
  @Id
  private long id;
  private String productName;
}

@Entity
@DiscriminatorValue("Book")
public class Book extends Product { 
  private String author;
  ...
}
@Entity
@DiscriminatorValue("Furniture")
public class Furniture extends Product {
  String color;
  ...
}

DTO

public class ProductDto {
  private long id;
  private String productName;
  ...
}

public class BookDto extends ProductDto {
  private String author;
  ...
}
public class FurnitureDto extends ProductDto {
   String color;
   ... 
}

Cartographe

@Mapper(uses = {BookMapper.class,FurnitureMapper.class})
public interface ProductMapper {
    
    ProductDto productToProductDto(Product product);
    Product productDtoToProduct(ProductDto productDto);
}

@Mapper
public interface BookMapper {
    BookDto bookToBookDto(Book book);
    Book bookDtoToBook(BookDto bookDto);
}

@Mapper
public interface FurnitureMapper {
    FurnitureDto furnitureToFurnitureDto(Furniture furniture);
    Furniture furnitureDtoToFurniture(FurnitureDto furnitureDto);
}

Un service

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    ProductRepository productRepository;
    @Autowired
    ProductMapper productMapper;

    @Override
    public List<ProductDto> getAllProducts() {
        List<ProductDto> listOfProducts = new ArrayList<>();
        productRepository.findAll().forEach(i -> 
        listOfProducts.add(productMapper.productToProductDto(i)));
        return listOfProducts;
    }
}

Édité

J'obtiens le résultat suivant après avoir mappé l'entité produit sur le produit dto. Il ne lie pas les données et n'inclut pas ses attributs enfants. La section mapper ci-dessus est -elle correcte?

[
    {
        "id": 0,
        "productName": null
    },
    {
        "id": 0,
        "productName": null
    },
    ...
]

Le résultat devrait cependant être comme ci-dessous:

[
    {
        "id": 11,
        "productName": ABC,
        "author":"James"
    },
    {
        "id": 22,
        "productName": XYZ,
        "color":"Oak"
    },
    ...
]

Réponses

2 TasosP. Dec 10 2020 at 05:01

TL; DR

Il n'y a aucun moyen propre de faire cela. La raison réside dans la sélection de la méthode de compilation par Java. Mais il existe une manière quelque peu propre d'utiliser le modèle de visiteur.

Pourquoi ça ne marche pas

Pendant que vous parcourez la liste des entités, qui contient différents types (Produit, Livre, Meubles), vous devez appeler une méthode de mappage différente pour chaque type (c'est-à-dire un mappeur MapStruct différent).

À moins que vous ne suiviez le instanceofchemin, comme le suggère Amir, et que vous ne sélectionniez explicitement le mappeur, vous devez utiliser la surcharge de méthode pour invoquer différentes méthodes de mappage par classe d'entité. Le problème est que Java sélectionnera la méthode surchargée au moment de la compilation et à ce stade, le compilateur ne verra qu'une liste d' Productobjets (ceux retournés par votre méthode de référentiel). Peu importe si MapStruct ou Spring ou votre propre code personnalisé essaie de le faire. C'est aussi pourquoi your ProductMapperest toujours invoqué: c'est le seul type visible à la compilation.

Utilisation du modèle de visiteur

Puisque nous devons choisir le bon mappeur manuellement , nous pouvons choisir la manière la plus propre ou la plus maintenable. C'est définitivement avisé.

Ma suggestion est d'utiliser le modèle de visiteur (en fait une variante de celui-ci) de la manière suivante:

Introduisez une nouvelle interface pour vos entités qui doivent être mappées:

public interface MappableEntity {

    public ProductDto map(EntityMapper mapper);
}

Vos entités devront implémenter cette interface, par exemple:

public class Book extends Product implements MappableEntity {
//...
@Override
    public ProductDto map(EntityMapper mapper) {
        return mapper.map(this);//This is the magic part. We choose which method to call because the parameter is this, which is a Book!
    }
}

EntityMapper est l'interface visiteur:

public interface EntityMapper {

    ProductDto map(Product entity);

    BookDto map(Book entity);

    FurnitureDto map(Furniture entity);

    // Add your next entity here
}

Enfin, vous avez besoin du MasterMapper:

// Not a class name I'm proud of
public class MasterMapper implements EntityMapper {

    // Inject your mappers here

    @Override
    public ProductDto map(Product product) {
        ProductMapper productMapper = Mappers.getMapper(ProductMapper.class);
        return productMapper.map(product);
    }

    @Override
    public BookDto map(Book product) {
        BookMapper productMapper = Mappers.getMapper(BookMapper.class);
        return productMapper.map(product);
    }

    @Override
    public FurnitureDto map(Furniture product) {
        FurnitureMapper productMapper = Mappers.getMapper(FurnitureMapper.class);
        return productMapper.map(product);
    }

    // Add your next mapper here

}

Votre méthode de service ressemblera alors à:

MasterMapper mm = new MasterMapper();
List<Product> listOfEntities = productRepository.findAll();
List<ProductDto> listOfProducts = new ArrayList<>(listOfEntities.size());
listOfEntities.forEach(i -> {
        if (i instanceof MappableEntity) {
            MappableEntity me = i;
            ProductDto dto = me.map(mm);
            listOfProducts.add(dto);
        } else {
            // Throw an AssertionError during development (who would run server VMs with -ea ?!?!)
            assert false : "Can't properly map " + i.getClass() + " as it's not implementing MappableEntity";
            // Use default mapper as a fallback
            final ProductDto defaultDto = Mappers.getMapper(ProductMapper.class).map(i);
            listOfProducts.add(defaultDto);
        }
     });
    return listOfProducts;

Vous pouvez ignorer les Mappers.getMapper()appels en toute sécurité : comme le problème n'est pas lié à Spring, j'ai créé un exemple de travail sur GitHub en utilisant les usines de MapStruct pour plus de simplicité. Vous injecterez simplement vos mappeurs en utilisant CDI.

1 AmirSchnell Dec 09 2020 at 16:44

Il s'agit exactement du même scénario que celui décrit dans ce problème .
Malheureusement, ce problème est encore ouvert et personne n'a encore fourni de solution. Donc je pense que ce que vous voulez n'est pas possible.

La seule façon de résoudre votre problème est de changer le code au sein de votre service.

Une chose que vous pouvez faire, comme mentionné dans le problème, est de tester chaque Productobjet avec instanceof:

@Autowired
BookMapper bookMapper;
@Autowired
FurnitureMapper furnitureMapper;

public List<ProductDto> getAllProducts() {
    List<ProductDto> listOfProducts = new ArrayList<>();
    List<Product> all = productRepository.findAll();
    for(Product product : all) {
        if(product instanceof Book) {
            listOfProducts.add(bookMapper.bookToBookDto((Book)product));
        } else if(product instanceof Furniture) {
            listOfProducts.add(furnitureMapper.furnitureToFurnitureDto((Furniture)product));
        }
    }
    return listOfProducts;
}

Vous pouvez également faire votre ProductMapperprolongation BookMapperet FurnitureMapper, pour ne pas avoir à les injecter.