ModelMapper: pulang pergi

gambar

Untuk alasan yang sudah diketahui, backend tidak dapat mengembalikan data dari repositori apa adanya. Yang paling terkenal - ketergantungan esensial tidak diambil dari dasar dalam bentuk di mana bagian depan dapat memahaminya. Di sini Anda dapat menambahkan kesulitan dengan parsing enum (jika bidang enum berisi parameter tambahan), dan banyak kesulitan lain yang timbul dari casting tipe otomatis (atau ketidakmampuan untuk secara otomatis melemparkannya). Ini menyiratkan kebutuhan untuk menggunakan Obyek Transfer Data - DTO, yang dapat dimengerti untuk bagian belakang dan depan.
Mengkonversi entitas ke DTO dapat dilakukan dengan berbagai cara. Anda dapat menggunakan perpustakaan, Anda dapat (jika proyek ini kecil) mengumpulkan sesuatu seperti ini:

@Component public class ItemMapperImpl implements ItemMapper { private final OrderRepository orderRepository; @Autowired public ItemMapperImpl(OrderRepository orderRepository) { this.orderRepository = orderRepository; } @Override public Item toEntity(ItemDto dto) { return new Item( dto.getId(), obtainOrder(dto.getOrderId()), dto.getArticle(), dto.getName(), dto.getDisplayName(), dto.getWeight(), dto.getCost(), dto.getEstimatedCost(), dto.getQuantity(), dto.getBarcode(), dto.getType() ); } @Override public ItemDto toDto(Item item) { return new ItemDto( item.getId(), obtainOrderId(item), item.getArticle(), item.getName(), item.getDisplayName(), item.getWeight(), item.getCost(), item.getEstimatedCost(), item.getQuantity(), item.getBarcode(), item.getType() ); } private Long obtainOrderId(Item item) { return Objects.nonNull(item.getOrder()) ? item.getOrder().getId() : null; } private Order obtainOrder(Long orderId) { return Objects.nonNull(orderId) ? orderRepository.findById(orderId).orElse(null) : null; } } 

Pemetaan yang ditulis sendiri semacam itu memiliki kelemahan yang jelas:

  1. Jangan mengukur.
  2. Saat menambahkan / menghapus bahkan bidang yang paling tidak penting, Anda harus mengedit mapper.

Oleh karena itu, solusi yang benar adalah dengan menggunakan pustaka mapper. Saya tahu modelmapper dan mapstruct. Karena saya bekerja dengan modelmapper, saya akan membicarakannya, tetapi jika Anda, pembaca saya, sangat mengenal mapstruct dan dapat mengetahui semua seluk beluk aplikasinya, silakan tulis artikel tentang itu, dan saya akan menjadi orang pertama yang menulisnya kepada saya (perpustakaan ini juga sangat menarik, tetapi belum ada waktu untuk memasukinya).

Jadi modelmapper.

Saya ingin segera mengatakan bahwa jika ada sesuatu yang tidak jelas bagi Anda, Anda dapat mengunduh proyek yang sudah selesai dengan tes yang berfungsi, tautan di akhir artikel.

Langkah pertama, tentu saja, menambahkan ketergantungan. Saya menggunakan gradle, tetapi mudah bagi Anda untuk menambahkan ketergantungan pada proyek pakar Anda.

 compile group: 'org.modelmapper', name: 'modelmapper', version: '2.3.2' 

Ini sudah cukup bagi mapper untuk bekerja. Selanjutnya, kita perlu membuat bin.

 @Bean public ModelMapper modelMapper() { ModelMapper mapper = new ModelMapper(); mapper.getConfiguration() .setMatchingStrategy(MatchingStrategies.STRICT) .setFieldMatchingEnabled(true) .setSkipNullEnabled(true) .setFieldAccessLevel(PRIVATE); return mapper; } 

Biasanya cukup dengan mengembalikan ModelMapper baru, tetapi tidak akan terlalu berlebihan untuk mengkonfigurasi mapper untuk kebutuhan kita. Saya menetapkan strategi pencocokan ketat, mengaktifkan pemetaan bidang, melewatkan bidang nol, dan mengatur tingkat akses pribadi ke bidang tersebut.

Selanjutnya, buat struktur entitas berikut. Kami akan memiliki Unicorn, yang akan memiliki sejumlah droid dalam pengajuannya, dan setiap droid akan memiliki jumlah Cupcakes tertentu.

Entitas
Induk abstrak:

 @MappedSuperclass @Setter @EqualsAndHashCode @NoArgsConstructor @AllArgsConstructor public abstract class AbstractEntity implements Serializable { Long id; LocalDateTime created; LocalDateTime updated; @Id @GeneratedValue public Long getId() { return id; } @Column(name = "created", updatable = false) public LocalDateTime getCreated() { return created; } @Column(name = "updated", insertable = false) public LocalDateTime getUpdated() { return updated; } @PrePersist public void toCreate() { setCreated(LocalDateTime.now()); } @PreUpdate public void toUpdate() { setUpdated(LocalDateTime.now()); } } 

Unicorn:

 @Entity @Table(name = "unicorns") @EqualsAndHashCode(callSuper = false) @Setter @AllArgsConstructor @NoArgsConstructor public class Unicorn extends AbstractEntity { private String name; private List<Droid> droids; private Color color; public Unicorn(String name, Color color) { this.name = name; this.color = color; } @Column(name = "name") public String getName() { return name; } @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "unicorn") public List<Droid> getDroids() { return droids; } @Column(name = "color") public Color getColor() { return color; } } 

Droid:

 @Setter @EqualsAndHashCode(callSuper = false) @Entity @Table(name = "droids") @AllArgsConstructor @NoArgsConstructor public class Droid extends AbstractEntity { private String name; private Unicorn unicorn; private List<Cupcake> cupcakes; private Boolean alive; public Droid(String name, Unicorn unicorn, Boolean alive) { this.name = name; this.unicorn = unicorn; this.alive = alive; } public Droid(String name, Boolean alive) { this.name = name; this.alive = alive; } @Column(name = "name") public String getName() { return name; } @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "unicorn_id") public Unicorn getUnicorn() { return unicorn; } @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "droid") public List<Cupcake> getCupcakes() { return cupcakes; } @Column(name = "alive") public Boolean getAlive() { return alive; } } 

Cupcake:

 @Entity @Table(name = "cupcakes") @Setter @EqualsAndHashCode(callSuper = false) @AllArgsConstructor @NoArgsConstructor public class Cupcake extends AbstractEntity { private Filling filling; private Droid droid; @Column(name = "filling") public Filling getFilling() { return filling; } @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "droid_id") public Droid getDroid() { return droid; } public Cupcake(Filling filling) { this.filling = filling; } } 


Kami akan mengonversi entitas ini ke DTO. Setidaknya ada dua pendekatan untuk mengubah dependensi dari entitas ke DTO. Satu menyiratkan hanya menyimpan ID bukan entitas, tetapi kemudian setiap entitas dari ketergantungan, jika perlu, kami akan menarik ID tambahan. Pendekatan kedua melibatkan menjaga ketergantungan DTO. Jadi, dalam pendekatan pertama, kami akan mengonversi Daftar droid menjadi Daftar droid (kami hanya menyimpan ID di daftar baru), dan dalam pendekatan kedua, kami akan menyimpan dalam Daftar droid.

DTO
Induk abstrak:

 @Data public abstract class AbstractDto implements Serializable { private Long id; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS") LocalDateTime created; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS") LocalDateTime updated; } 

Unicorn:

 @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class UnicornDto extends AbstractDto { private String name; private List<DroidDto> droids; private String color; } 

DroidDto:

 @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class DroidDto extends AbstractDto { private String name; private List<CupcakeDto> cupcakes; private UnicornDto unicorn; private Boolean alive; } 

CupcakeUntuk:

 @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class CupcakeDto extends AbstractDto { private String filling; private DroidDto droid; } 


Untuk menyesuaikan mapper dengan kebutuhan kita, kita perlu membuat kelas wrapper kita sendiri dan mendefinisikan kembali logika untuk memetakan koleksi. Untuk melakukan ini, kita membuat kelas komponen UnicornMapper, memetakan mapper kita secara otomatis di sana dan mendefinisikan kembali metode yang kita butuhkan.

Versi kelas pembungkus yang paling sederhana terlihat seperti ini:

 @Component public class UnicornMapper { @Autowired private ModelMapper mapper; @Override public Unicorn toEntity(UnicornDto dto) { return Objects.isNull(dto) ? null : mapper.map(dto, Unicorn.class); } @Override public UnicornDto toDto(Unicorn entity) { return Objects.isNull(entity) ? null : mapper.map(entity, UnicornDto.class); } } 

Sekarang cukup bagi kita untuk autowire mapper kita ke beberapa layanan dan menarik menggunakan metode toDto dan toEntity. Mapper akan mengubah entitas yang ditemukan di objek menjadi DTO, DTO - menjadi entitas.

 @Service public class UnicornServiceImpl implements UnicornService { private final UnicornRepository repository; private final UnicornMapper mapper; @Autowired public UnicornServiceImpl(UnicornRepository repository, UnicornMapper mapper) { this.repository = repository; this.mapper = mapper; } @Override public UnicornDto save(UnicornDto dto) { return mapper.toDto(repository.save(mapper.toEntity(dto))); } @Override public UnicornDto get(Long id) { return mapper.toDto(repository.getOne(id)); } } 

Tetapi jika kita mencoba mengonversi sesuatu dengan cara ini, dan memanggil, misalnya, ke Strtring, maka kita akan mendapatkan StackOverflowException, dan inilah alasannya: UnicornDto berisi daftar DroidDto, yang berisi UnicornDto, yang berisi DroidDto, dan seterusnya hingga saat itu sampai memori tumpukan habis. Oleh karena itu, untuk dependensi terbalik, saya biasanya menggunakan unicorn UnicornDto, tetapi Long unicornId. Dengan cara ini, kami tetap berhubungan dengan Unicorn, tetapi memotong ketergantungan siklik. Mari kita perbaiki DTO kita sehingga alih-alih membalikkan DTO, mereka menyimpan ID dependensi mereka.

 @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class DroidDto extends AbstractDto { ... //private UnicornDto unicorn; private Long unicornId; ... } 

dan sebagainya.

Tetapi sekarang, jika kita memanggil DroidMapper, kita mendapatkan unicornId == null. Ini karena ModelMapper tidak dapat menentukan dengan tepat Long. Dan itu tidak mengganggunya. Dan kita harus mengatur pembuat peta yang diperlukan untuk mengajari mereka cara memetakan entitas dalam ID.

Kami ingat bahwa dengan setiap nampan setelah inisialisasi, Anda dapat bekerja secara manual.

  @PostConstruct public void setupMapper() { mapper.createTypeMap(Droid.class, DroidDto.class) .addMappings(m -> m.skip(DroidDto::setUnicornId)).setPostConverter(toDtoConverter()); mapper.createTypeMap(DroidDto.class, Droid.class) .addMappings(m -> m.skip(Droid::setUnicorn)).setPostConverter(toEntityConverter()); } 

Di @PostConstruct kita akan menetapkan aturan di mana kita menunjukkan bidang mana yang tidak boleh disentuh oleh mapper, karena bagi mereka kita akan menentukan logika sendiri. Dalam kasus kami, ini adalah definisi unicornId di DTO dan definisi Unicorn pada intinya (karena mapper juga tidak tahu apa yang harus dilakukan dengan Long unicornId).

TypeMap - ini adalah aturan di mana kami menentukan semua nuansa pemetaan, dan juga, mengatur konverter. Kami menunjukkan bahwa untuk mengkonversi dari Droid ke DroidDto, kami melewatkan setUnicornId, dan dalam konversi terbalik, kami melewati setUnicorn. Kita semua akan mengonversi di toDtoConverter () converter untuk UnicornDto dan toEntityConverter () untuk Unicorn. Kita harus menggambarkan konverter ini di komponen kami.

Post-converter yang paling sederhana terlihat seperti ini:

  Converter<UnicornDto, Unicorn> toEntityConverter() { return MappingContext::getDestination; } 

Kami perlu memperluas fungsinya:

  public Converter<UnicornDto, Unicorn> toEntityConverter() { return context -> { UnicornDto source = context.getSource(); Unicorn destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; } 

Kami melakukan hal yang sama dengan konverter terbalik:

  public Converter<Unicorn, UnicornDto> toDtoConverter() { return context -> { Unicorn source = context.getSource(); UnicornDto destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; } 

Bahkan, kami cukup memasukkan metode tambahan ke setiap post-converter, di mana kami menulis logika kami sendiri untuk bidang yang hilang.

  public void mapSpecificFields(Droid source, DroidDto destination) { destination.setUnicornId(Objects.isNull(source) || Objects.isNull(source.getId()) ? null : source.getUnicorn().getId()); } void mapSpecificFields(DroidDto source, Droid destination) { destination.setUnicorn(unicornRepository.findById(source.getUnicornId()).orElse(null)); } 

Saat memetakan di DTO, kami menetapkan ID entitas. Saat memetakan di DTO, kami mendapatkan entitas dari repositori dengan ID.

Dan itu dia.

Saya menunjukkan minimum yang diperlukan untuk mulai bekerja dengan modelmapper dan tidak secara khusus memperbaiki kode. Jika Anda, pembaca, memiliki sesuatu untuk ditambahkan ke artikel saya, saya akan senang mendengar kritik yang membangun.

Proyek ini dapat dilihat di sini:
Proyek di GitHub.

Penggemar kode bersih mungkin sudah melihat peluang untuk mengarahkan banyak komponen kode ke dalam abstraksi. Jika Anda salah satunya, saya sarankan di bawah kucing.

Naikkan tingkat abstraksi
Untuk memulainya, kita mendefinisikan antarmuka untuk metode dasar kelas wrapper.

 public interface Mapper<E extends AbstractEntity, D extends AbstractDto> { E toEntity(D dto); D toDto(E entity); } 

Kami mewarisinya dari kelas abstrak.

 public abstract class AbstractMapper<E extends AbstractEntity, D extends AbstractDto> implements Mapper<E, D> { @Autowired ModelMapper mapper; private Class<E> entityClass; private Class<D> dtoClass; AbstractMapper(Class<E> entityClass, Class<D> dtoClass) { this.entityClass = entityClass; this.dtoClass = dtoClass; } @Override public E toEntity(D dto) { return Objects.isNull(dto) ? null : mapper.map(dto, entityClass); } @Override public D toDto(E entity) { return Objects.isNull(entity) ? null : mapper.map(entity, dtoClass); } Converter<E, D> toDtoConverter() { return context -> { E source = context.getSource(); D destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; } Converter<D, E> toEntityConverter() { return context -> { D source = context.getSource(); E destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; } void mapSpecificFields(E source, D destination) { } void mapSpecificFields(D source, E destination) { } } 

Konverter pasca dan metode untuk mengisi bidang tertentu dapat dengan aman dikirim ke sana. Juga, buat dua objek bertipe Class dan sebuah konstruktor untuk menginisialisasi mereka:

  private Class<E> entityClass; private Class<D> dtoClass; AbstractMapper(Class<E> entityClass, Class<D> dtoClass) { this.entityClass = entityClass; this.dtoClass = dtoClass; } 

Sekarang jumlah kode di DroidMapper dikurangi menjadi sebagai berikut:

 @Component public class DroidMapper extends AbstractMapper<Droid, DroidDto> { private final ModelMapper mapper; private final UnicornRepository unicornRepository; @Autowired public DroidMapper(ModelMapper mapper, UnicornRepository unicornRepository) { super(Droid.class, DroidDto.class); this.mapper = mapper; this.unicornRepository = unicornRepository; } @PostConstruct public void setupMapper() { mapper.createTypeMap(Droid.class, DroidDto.class) .addMappings(m -> m.skip(DroidDto::setUnicornId)).setPostConverter(toDtoConverter()); mapper.createTypeMap(DroidDto.class, Droid.class) .addMappings(m -> m.skip(Droid::setUnicorn)).setPostConverter(toEntityConverter()); } @Override public void mapSpecificFields(Droid source, DroidDto destination) { destination.setUnicornId(getId(source)); } private Long getId(Droid source) { return Objects.isNull(source) || Objects.isNull(source.getId()) ? null : source.getUnicorn().getId(); } @Override void mapSpecificFields(DroidDto source, Droid destination) { destination.setUnicorn(unicornRepository.findById(source.getUnicornId()).orElse(null)); } } 

Seorang mapper tanpa bidang tertentu umumnya tampak sederhana:

 @Component public class UnicornMapper extends AbstractMapper<Unicorn, UnicornDto> { @Autowired public UnicornMapper() { super(Unicorn.class, UnicornDto.class); } } 

Source: https://habr.com/ru/post/id438808/


All Articles