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
これを行うためのクリーンな方法はありません。その理由は、Javaのコンパイル時のメソッド選択にあります。しかし、ビジターパターンを使用するためのややクリーンな方法があります。
なぜ機能しないのか
さまざまなタイプ(Product、Book、Furniture)を含むエンティティのリストを反復処理するときに、タイプごとに異なるマッピングメソッド(つまり、異なるMapStructマッパー)を呼び出す必要があります。
instanceof
Amirが提案しているように、マッパーを明示的に選択しない限り、メソッドのオーバーロードを使用して、エンティティクラスごとに異なるマッピングメソッドを呼び出す必要があります。問題は、Javaがコンパイル時にオーバーロードされたメソッドを選択し、その時点で、コンパイラが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
ので、それらを注入する必要はありません。