Wie ordne ich Eltern und Kinder mit Mapstruct in Spring Boot zu?
Ich habe Eltern (Produkt) und Kinder (Buch, Möbel) und möchte die Produktentität dem Produkt DTO zuordnen. Wie Sie sehen können, wird das Produkt zugeordnet und in einer einzelnen Tabelle in der Datenbank gespeichert. Wie kann ich das übergeordnete Produkt zuordnen, das zusätzliche Details zu seinem untergeordneten Produkt enthält?
Ich habe mir das , das und das angeschaut , um eine Vorstellung zu bekommen, aber kein Glück
Entität
@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;
...
}
Mapper
@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);
}
Bedienung
@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;
}
}
Bearbeitet
Ich erhalte das folgende Ergebnis, nachdem ich die Produktentität dem Produkt dto zugeordnet habe. Es bindet die Daten nicht und enthält keine untergeordneten Attribute. Korrigiert der obige Mapper- Abschnitt?
[
{
"id": 0,
"productName": null
},
{
"id": 0,
"productName": null
},
...
]
Das Ergebnis sollte jedoch wie folgt aussehen:
[
{
"id": 11,
"productName": ABC,
"author":"James"
},
{
"id": 22,
"productName": XYZ,
"color":"Oak"
},
...
]
Antworten
TL; DR
Es gibt keinen sauberen Weg, dies zu tun. Der Grund liegt in der Auswahl der Java-Methode zur Kompilierungszeit. Es gibt jedoch eine etwas saubere Art, das Besuchermuster zu verwenden.
Warum funktioniert es nicht?
Während Sie die Liste der Entitäten durchlaufen, die verschiedene Typen (Produkt, Buch, Möbel) enthält, müssen Sie für jeden Typ eine andere Zuordnungsmethode aufrufen (dh einen anderen MapStruct-Mapper).
Sofern Sie nicht instanceof
wie von Amir vorgeschlagen vorgehen und den Mapper explizit auswählen, müssen Sie die Methodenüberladung verwenden, um verschiedene Zuordnungsmethoden pro Entitätsklasse aufzurufen. Das Problem ist, dass Java die überladene Methode zur Kompilierungszeit auswählt und der Compiler zu diesem Zeitpunkt nur eine Liste von Product
Objekten sieht (die von Ihrer Repository-Methode zurückgegeben wurden). Es spielt keine Rolle, ob MapStruct oder Spring oder Ihr eigener benutzerdefinierter Code dies versuchen. Dies ist auch der Grund, warum Ihr ProductMapper
immer aufgerufen wird: Es ist der einzige Typ, der zur Kompilierungszeit sichtbar ist.
Verwenden des Besuchermusters
Da wir den richtigen Mapper manuell auswählen müssen , können wir entscheiden, welcher Weg sauberer oder wartbarer ist. Dies ist definitiv eine Meinung.
Mein Vorschlag ist, das Besuchermuster (eigentlich eine Variation davon) folgendermaßen zu verwenden:
Führen Sie eine neue Schnittstelle für Ihre Entitäten ein, die zugeordnet werden müssen:
public interface MappableEntity {
public ProductDto map(EntityMapper mapper);
}
Ihre Entitäten müssen diese Schnittstelle implementieren, zum Beispiel:
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 ist die Besucheroberfläche:
public interface EntityMapper {
ProductDto map(Product entity);
BookDto map(Book entity);
FurnitureDto map(Furniture entity);
// Add your next entity here
}
Schließlich benötigen Sie den 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
}
Ihre Servicemethode sieht dann folgendermaßen aus:
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;
Sie können Mappers.getMapper()
Anrufe ignorieren : Da das Problem nicht mit Spring zusammenhängt, habe ich der Einfachheit halber ein funktionierendes Beispiel für GitHub mit den Fabriken von MapStruct erstellt. Sie injizieren Ihre Mapper einfach mit CDI.
Dies ist genau das gleiche Szenario wie in dieser Ausgabe beschrieben .
Leider ist dieses Problem derzeit noch offen und bisher hat noch niemand eine Lösung bereitgestellt. Ich denke also, was du willst, ist nicht möglich.
Die einzige Möglichkeit, Ihr Problem zu lösen, besteht darin, den Code in Ihrem Dienst zu ändern.
Wie in der Ausgabe erwähnt, können Sie jedes Product
Objekt testen mit 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;
}
Sie können auch ProductMapper
verlängern BookMapper
und FurnitureMapper
, damit Sie diese nicht injizieren müssen.