Spring BootでMapstructを使用して親と子をマップする方法は?

Dec 01 2020

親(製品)と子(本、家具)があり、製品エンティティを製品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"
    },
    ...
]

回答

2 TasosP. Dec 10 2020 at 05:01

TL; DR

これを行うためのクリーンな方法はありません。その理由は、Javaのコンパイル時のメソッド選択にあります。しかし、ビジターパターンを使用するためのややクリーンな方法があります。

なぜ機能しないのか

さまざまなタイプ(Product、Book、Furniture)を含むエンティティのリストを反復処理するときに、タイプごとに異なるマッピングメソッド(つまり、異なるMapStructマッパー)を呼び出す必要があります。

instanceofAmirが提案しているように、マッパーを明示的に選択しない限り、メソッドのオーバーロードを使用して、エンティティクラスごとに異なるマッピングメソッドを呼び出す必要があります。問題は、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を使用してマッパーを注入するだけです。

1 AmirSchnell Dec 09 2020 at 16:44

これは、この号で説明されているシナリオとまったく同じです。
残念ながら、この問題は現在も未解決であり、これまでのところ誰も解決策を提供していません。だから私はあなたが望むことは不可能だと思います。

問題を解決する唯一の方法は、サービス内のコードを変更することです。

問題で言及されているように、あなたができる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ので、それらを注入する必要はありません。