Bagaimana cara memetakan orang tua dan anak menggunakan Mapstruct di Spring Boot?

Dec 01 2020

Saya memiliki orang tua (produk) dan anak (buku, furnitur), dan ingin memetakan entitas produk ke produk DTO. Seperti yang Anda lihat, produk dipetakan dan disimpan dalam satu tabel di database. Bagaimana saya bisa memetakan induk, produk, yang memiliki detail tambahan tentang anaknya?

Saya telah melihat ini , ini dan ini untuk mendapatkan beberapa ide tetapi tidak berhasil

Kesatuan

@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;
   ... 
}

Pembuat peta

@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);
}

Layanan

@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;
    }
}

Diedit

Saya mendapatkan hasil berikut setelah memetakan entitas produk ke produk dto. Itu tidak mengikat data dan tidak menyertakan atribut turunannya. Apakah bagian mapper di atas benar?

[
    {
        "id": 0,
        "productName": null
    },
    {
        "id": 0,
        "productName": null
    },
    ...
]

Namun hasilnya harus seperti di bawah ini:

[
    {
        "id": 11,
        "productName": ABC,
        "author":"James"
    },
    {
        "id": 22,
        "productName": XYZ,
        "color":"Oak"
    },
    ...
]

Jawaban

2 TasosP. Dec 10 2020 at 05:01

TL; DR

Tidak ada cara yang bersih untuk melakukan ini. Alasannya terletak pada pemilihan metode waktu kompilasi Java. Tetapi ada cara yang agak bersih dalam menggunakan pola pengunjung.

Mengapa tidak berhasil

Saat Anda mengulang daftar entitas, yang berisi berbagai jenis (Produk, Buku, Furnitur), Anda perlu memanggil metode pemetaan yang berbeda untuk setiap jenis (yaitu, pemeta MapStruct yang berbeda).

Kecuali Anda mengikuti instanceofcaranya, seperti yang disarankan Amir, dan secara eksplisit memilih mapper, Anda perlu menggunakan metode overloading untuk memanggil metode pemetaan yang berbeda per kelas entitas. Masalahnya adalah Java akan memilih metode kelebihan beban pada waktu kompilasi dan pada saat itu, kompilator hanya melihat daftar Productobjek (yang dikembalikan oleh metode repositori Anda). Tidak masalah jika MapStruct atau Spring atau kode kustom Anda sendiri mencoba melakukannya. Ini juga mengapa your ProductMapperselalu dipanggil: itu satu-satunya jenis yang terlihat pada waktu kompilasi.

Menggunakan pola pengunjung

Karena kita perlu memilih mapper yang tepat secara manual , kita bisa memilih cara mana yang lebih bersih atau lebih mudah dirawat. Ini pasti beropini.

Saran saya adalah menggunakan pola pengunjung (sebenarnya adalah variasinya) dengan cara berikut:

Perkenalkan antarmuka baru untuk entitas Anda yang perlu dipetakan:

public interface MappableEntity {

    public ProductDto map(EntityMapper mapper);
}

Entitas Anda perlu mengimplementasikan antarmuka ini, misalnya:

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 adalah antarmuka pengunjung:

public interface EntityMapper {

    ProductDto map(Product entity);

    BookDto map(Book entity);

    FurnitureDto map(Furniture entity);

    // Add your next entity here
}

Terakhir, Anda membutuhkan 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

}

Metode layanan Anda akan terlihat seperti ini:

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;

Anda dapat mengabaikan Mappers.getMapper()panggilan dengan aman : karena masalahnya tidak terkait dengan Spring, saya membuat contoh yang berfungsi di GitHub menggunakan pabrik MapStruct untuk kesederhanaan. Anda hanya akan menyuntikkan pembuat peta Anda menggunakan CDI.

1 AmirSchnell Dec 09 2020 at 16:44

Ini persis skenario yang sama seperti yang dijelaskan dalam masalah ini .
Sayangnya masalah ini saat ini masih terbuka dan belum ada yang memberikan solusi sejauh ini. Jadi saya pikir apa yang Anda inginkan tidak mungkin.

Satu-satunya cara untuk menyelesaikan masalah Anda adalah dengan mengubah kode dalam layanan Anda.

Satu hal yang dapat Anda lakukan, seperti yang disebutkan dalam masalah ini, adalah menguji setiap Productobjek dengan 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;
}

Anda juga bisa membuat ProductMapperperpanjangan BookMapperdan FurnitureMapper, jadi Anda tidak perlu menyuntikkannya.