Comment mapper les parents et les enfants à l'aide de Mapstruct dans Spring Boot?
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
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 instanceof
chemin, 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' Product
objets (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 ProductMapper
est 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.
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 Product
objet 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 ProductMapper
prolongation BookMapper
et FurnitureMapper
, pour ne pas avoir à les injecter.