如何在 Spring Boot 中使用 Mapstruct 映射父项和子项?
How to map parent and children using Mapstruct in Spring Boot?
我有父项(产品)和子项(书籍、家具),并且想将产品实体映射到产品 DTO。如您所见,产品被映射并存储在数据库中的单个 table 中。我如何映射具有其子项额外详细信息的父项、产品?
我看过 , this and 以了解一些想法,但运气不好
实体
@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 后,我得到以下结果。它不绑定数据,也不包括其子属性。以上 mapper 部分是否正确?
[
{
"id": 0,
"productName": null
},
{
"id": 0,
"productName": null
},
...
]
结果应该如下所示:
[
{
"id": 11,
"productName": ABC,
"author":"James"
},
{
"id": 22,
"productName": XYZ,
"color":"Oak"
},
...
]
这与 this issue 中描述的情况完全相同。
不幸的是,这个问题目前仍未解决,到目前为止还没有人提供解决方案。所以我觉得你想要的是不可能的。
解决问题的唯一方法是更改服务中的代码。
您可以做的事情,如问题中所述,是用 instanceof
:
测试每个 Product
对象
@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
,这样您就不必注入它们了。
TL;DR
没有干净的方法来做到这一点。原因在于Java的编译时方法selection。但是有一种比较干净的方式来使用访问者模式。
为什么它不起作用
当您迭代包含不同类型(Product、Book、Furniture)的实体列表时,您需要为每种类型调用不同的映射方法(即不同的 MapStruct 映射器)。
除非您按照 Amir 的建议采用 instanceof
方式,并且显式 select 映射器,否则您需要使用方法重载来为每个实体 class 调用不同的映射方法。问题是 Java 将在编译时 select 重载方法,此时,编译器只会看到 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 注入您的映射器。
我有父项(产品)和子项(书籍、家具),并且想将产品实体映射到产品 DTO。如您所见,产品被映射并存储在数据库中的单个 table 中。我如何映射具有其子项额外详细信息的父项、产品?
我看过
实体
@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 后,我得到以下结果。它不绑定数据,也不包括其子属性。以上 mapper 部分是否正确?
[
{
"id": 0,
"productName": null
},
{
"id": 0,
"productName": null
},
...
]
结果应该如下所示:
[
{
"id": 11,
"productName": ABC,
"author":"James"
},
{
"id": 22,
"productName": XYZ,
"color":"Oak"
},
...
]
这与 this issue 中描述的情况完全相同。
不幸的是,这个问题目前仍未解决,到目前为止还没有人提供解决方案。所以我觉得你想要的是不可能的。
解决问题的唯一方法是更改服务中的代码。
您可以做的事情,如问题中所述,是用 instanceof
:
Product
对象
@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
,这样您就不必注入它们了。
TL;DR
没有干净的方法来做到这一点。原因在于Java的编译时方法selection。但是有一种比较干净的方式来使用访问者模式。
为什么它不起作用
当您迭代包含不同类型(Product、Book、Furniture)的实体列表时,您需要为每种类型调用不同的映射方法(即不同的 MapStruct 映射器)。
除非您按照 Amir 的建议采用 instanceof
方式,并且显式 select 映射器,否则您需要使用方法重载来为每个实体 class 调用不同的映射方法。问题是 Java 将在编译时 select 重载方法,此时,编译器只会看到 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 注入您的映射器。