Spring Boot에서 Mapstruct를 사용하여 부모와 자식을 매핑하는 방법은 무엇입니까?
부모 (제품)와 자식 (책, 가구)이 있고 제품 엔티티를 제품 DTO에 매핑하고 싶습니다. 보시다시피 제품은 데이터베이스의 단일 테이블에 매핑되고 저장됩니다. 하위 항목에 대한 추가 세부 정보가있는 상위 제품을 매핑하려면 어떻게해야합니까?
나는 이것 , 이것 그리고 이것을 몇 가지 아이디어를 얻기 위해 봤지만 운이 없다
실재
@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(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);
}
서비스
@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;
}
}
수정 됨
제품 엔터티를 제품 dto에 매핑 한 후 다음 결과를 얻습니다. 데이터를 바인딩하지 않고 하위 속성을 포함하지 않습니다. 위의 매퍼 섹션이 맞습니까?
[
{
"id": 0,
"productName": null
},
{
"id": 0,
"productName": null
},
...
]
그러나 결과는 다음과 같아야합니다.
[
{
"id": 11,
"productName": ABC,
"author":"James"
},
{
"id": 22,
"productName": XYZ,
"color":"Oak"
},
...
]
답변
TL; DR
이를위한 깨끗한 방법 은 없습니다 . 그 이유는 자바의 컴파일 타임 메소드 선택에 있습니다. 그러나 방문자 패턴을 사용 하는 다소 깔끔한 방법이 있습니다.
작동하지 않는 이유
다른 유형 (제품, 책, 가구)을 포함하는 엔티티 목록을 반복하는 동안 각 유형에 대해 다른 매핑 메서드 (즉, 다른 MapStruct 매퍼)를 호출해야합니다.
instanceof
Amir가 제안한대로 매퍼를 명시 적으로 선택 하지 않는 한 메서드 오버로딩을 사용하여 엔티티 클래스별로 다른 매핑 메서드를 호출해야합니다. 문제는 자바가 컴파일 타임에 오버로드 된 메서드를 선택하고 그 시점에서 컴파일러는 Product
객체 목록 (저장소 메서드에서 반환 된 것) 만 본다는 것입니다. MapStruct 또는 Spring 또는 자체 사용자 정의 코드가 그렇게하려고 시도하는지는 실제로 중요하지 않습니다. 이것이 당신 ProductMapper
이 항상 호출되는 이유이기도합니다 . 컴파일 타임에 보이는 유일한 유형입니다.
방문자 패턴 사용
올바른 매퍼를 수동으로 선택해야하므로 어느 방법이 더 깔끔 하거나 유지 관리 가 더 쉬운 지 선택해야합니다 . 이것은 확실히 의견입니다.
내 제안은 방문자 패턴 (실제로는 변형)을 다음과 같은 방식으로 사용하는 것입니다.
매핑해야하는 엔터티에 대한 새 인터페이스를 소개합니다.
public interface MappableEntity {
public ProductDto map(EntityMapper mapper);
}
엔터티는이 인터페이스를 구현해야합니다. 예를 들면 다음과 같습니다.
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는 방문자 인터페이스입니다.
public interface EntityMapper {
ProductDto map(Product entity);
BookDto map(Book entity);
FurnitureDto map(Furniture entity);
// Add your next entity here
}
마지막으로 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
}
서비스 방법은 다음과 같습니다.
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;
Mappers.getMapper()
호출 은 무시해도됩니다 .이 문제는 Spring과 관련이 없기 때문에 간단하게 MapStruct의 팩토리를 사용하여 GitHub 에서 작업 예제를 만들었습니다 . CDI를 사용하여 매퍼를 주입하기 만하면됩니다.
이것은 이 문제 에서 설명한 것과 똑같은 시나리오 입니다.
안타깝게도이 문제는 현재 아직 진행 중이며 지금까지 아무도 해결책을 제공하지 않았습니다. 그래서 나는 당신이 원하는 것이 불가능하다고 생각합니다.
문제를 해결하는 유일한 방법은 서비스 내의 코드를 변경하는 것입니다.
문제에서 언급했듯이 OPne 작업은 다음을 사용하여 모든 Product
개체 를 테스트하는 것입니다 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;
}
당신은 또한 당신이 만들 수있는 ProductMapper
확장 BookMapper
하고 FurnitureMapper
당신이 그를 주입 할 필요가 없습니다.