Mély merülés a Mapstruct @ Spring

(Miguel Duque) (2020. december 14.)

Fotó: Beau Swierstra az Unsplash

Java-fejlesztőként határozottan tudod, hogy a POJO-k leképezése a többrétegű alkalmazások fejlesztésekor szokásos feladat.
Ezeknek a leképezéseknek a kézi megírása mellett unalmas és A fejlesztők számára kellemetlen feladat szintén hibára hajlamos.

MapStruct egy nyílt forráskódú Java könyvtár, amely leképező osztály megvalósításokat generál a fordítás során biztonságos és egyszerű mód.

A cikk során követünk egy példát arra, hogyan lehet kihasználni ezt a nagy teljesítményű könyvtárat, hogy jelentősen csökkentsük a kézzel rendszeresen írt kazán kód kódját.

Index

  • Alapvető leképezések
  • Különböző tulajdonságnevek hozzárendelése
  • Minden tulajdonság megfeleltetése
  • Gyermek entitás tulajdonságának feltérképezése
  • A teljes gyermek entitás leképezése – másik leképező segítségével
  • Térképezés egyéni módszerrel
  • @BeforeMapping @AfterMapping
  • Térképezés egy további paraméterrel
  • Függőség-injektálás a leképezési módszerekhez
  • Frissítések
  • Javítások javítása

Alapvető leképezések

Kezdjük az alkalmazásunkat egy alapmodellel, amely a Doctor osztályt tartalmazza. Szolgáltatásunk lekéri ezt az osztályt a modellrétegből, majd visszaad egy DoctorDto osztály.

Modellosztály:

@Data
public class Doctor {
private int id;
private String name;
}

Dto osztály:

@Data
public class DoctorDto {
private int id;
private String name;
}

Ehhez létre kell hoznunk a Mapper felületünket:

@Mapper(componentModel = "spring")
public interface DoctorMapper {
DoctorDto toDto(Doctor doctor);
}

Mivel mindkét osztálynak ugyanazok a tulajdonságnevei vannak ( id és név ), a mapstruct magában foglalja a létrehozott osztály mindkét mezőjének leképezését:

@Component
public class DoctorMapperImpl implements DoctorMapper {

@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}

DoctorDto doctorDto = new DoctorDto();

doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );

return doctorDto;
}
}

componentModel hozzáadásával = „Tavasz” , a létrehozott leképező tavaszi bab lesz, és az @Autowired feljegyzéssel lekérhető, mint bármely más bab:

@Service
public class DoctorService {

private final DoctorMapper doctorMapper;
private final DoctorRepository doctorRepository;

@Autowired
public DoctorService(DoctorMapper doctorMapper) {
this.doctorMapper = doctorMapper;
this.doctorRepository = doctorRepository;
}

public DoctorDto getDoctor(Integer id) {
Doctor doctor = doctorRepository.findById(id);
return doctorMapper.toDto(doctor);
}
}

Különböző tulajdonságnév leképezése

Ha a Doctor osztály:

@Data
public class Doctor {
private int id;
private String name;
private String phone;
}

Feltérképezendő névjegy hez a doktor Dto :

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
}

A térképkészítőnk nem fogja tudni automatikusan feltérképezni. Ehhez létre kell hoznunk egy szabályt ehhez a leképezéshez:

@Mapper(componentModel = "spring")
public interface DoctorMapper {

@Mapping(source = "phone", target = "contact")
DoctorDto toDto(Doctor doctor);
}

Minden tulajdonság megfeleltetése

Ha szeretnénk annak biztosítása érdekében, hogy ne felejtsük el megcélozni a céltulajdonságokat, konfigurálhatjuk az unmappedTargetPolicy opciót a térképkészítőn:

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface DoctorMapper {

@Mapping(source = "phone", target = "contact")
DoctorDto toDto(Doctor doctor);
}

Ezzel a konfigurációval, ha eltávolítjuk

@Mapping (source = “telefon”, target = “contact”)

kódunk meghiúsul a fordítás során a hiba:

Nem hozzárendelt céltulajdonság: „contact”. DoctorDto toDto (Doctor doctor);

Ha valamilyen oknál fogva figyelmen kívül akarunk hagyni egy célt tulajdonság, hozzáadhatjuk:

@Mapping (target = “contact”, ignore = true)

Hasonlóképpen garantálhatjuk, hogy az összes forrás tulajdonságok az unmappedSourcePolicy beállítás konfigurálásával vannak feltérképezve.

Gyermekalany tulajdonságának leképezése

Legtöbbször a szükséges osztály a térkép gyermek tárgyakat tartalmaz . Például:

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
}@Data
public class Speciality {
private int id;
private String name;
}

És a Dto-ban a teljes különlegesség helyett csak a neve kell:

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
}

Ez a helyzet a mapstruct esetében is egyértelmű:

@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
DoctorDto toDto(Doctor doctor);

A teljes gyermek entitás feltérképezése – másik használata mapper

Az előzőekhez hasonlóan a Doctor osztályunk gyermekobjektummal rendelkezik Cím :

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
private Address address;
}

Ebben az esetben azonban egy új objektum a DoctorDto osztályunkban:

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
private AddressDto address;
}

hajtsa végre az Address és a AddressDto osztály leképezését, létre kell hoznunk egy másik mapper felületet:

@Mapper(componentModel = "spring")
public interface AddressMapper {
AddressDto toDto(Address address);
}

Ezután a DoctorMapper meg kell győződnünk arról, hogy ezt a leképezőt használják a Doctor – DoctorDto .Ez a leképező konfigurációjának “ uses” opciójával tehető meg:

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {AddressMapper.class})
public interface DoctorMapper {

@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
DoctorDto toDto(Doctor doctor);
}

Meglátjuk, hogy a DoctorMapperImpl automatikusan bekapcsolja és felhasználja a AddressMapper :

@Component
public class DoctorMapperImpl implements DoctorMapper {

@Autowired
private AddressMapper addressMapper;

@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}

DoctorDto doctorDto = new DoctorDto();

doctorDto.setSpecialityName( doctorSpecialityName(doctor));
doctorDto.setContact( doctor.getPhone() );
doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );
doctorDto.setAddress(
addressMapper.toDto( doctor.getAddress() ) );


return doctorDto;
}

...
}

Feltérképezés egyéni módszerrel

Adjuk hozzá a betegek listáját a Doctor osztály:

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
private Address address;
private List patients;
}

Azonban a DoctorDto csak a betegek számát akarjuk meg:

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
private AddressDto address;
private int numPatients;
}

A leképezéshez 2 szükséges dolgok:

  • Egyéni módszer a @Név feliratozással
  • A qualifiedByName konfiguráció a leképezési kommentárban
@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {AddressMapper.class})
public interface DoctorMapper {

@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
@Mapping(
source = "patients",
target = "numPatients",
qualifiedByName = "countPatients")

DoctorDto toDto(Doctor doctor);

@Named("countPatients")
default int getNumPatients(List patients) {
if(patients == null) {
return 0;
}
return patients.size();
}
}

@BeforeMapping @AfterMapping

Az előző példa (leképezés Sorolja fel a betegeket int számPáciensek ) a @BeforeMapping és @AfterMapping

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {AddressMapper.class})
public interface DoctorMapper {

@BeforeMapping
default void setList(Doctor doctor) {
if (doctor != null && doctor.getPatients() == null) {
doctor.setPatients(new ArrayList());
}
}

@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
@Mapping(target = "numPatients", ignore = true)
DoctorDto toDto(Doctor doctor);

@AfterMapping
default void setNumPatients(Doctor doctor,
@MappingTarget DoctorDto doctorDto) {
doctorDto.setNumPatients(doctor.getPatients().size());
}
}

Ezeket a módszereket a létrehozott leképezési módszerünk elején és végén hívjuk meg:

@Override
public DoctorDto toDto(Doctor doctor) {
setList( doctor );

if ( doctor == null ) {
return null;
}

DoctorDto doctorDto = new DoctorDto();

doctorDto.setSpecialityName( doctorSpecialityName( doctor ) );
doctorDto.setContact( doctor.getPhone() );
doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );
doctorDto.setAddress( addressMapper.toDto(doctor.getAddress()));

setNumPatients( doctor, doctorDto );

return doctorDto;
}

Térképezés egy további paraméterrel

Ellenőrizzük, hogyan kell kezelni azokat a helyzeteket, amikor a leképezőnek további paramétert kell kapnia az Entitása mellett.

Ebben az esetben az orvos osztálya csak a város azonosítóját kaphatja meg:

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
private Address address;
private List patients;
private int cityId;
}

De a város nevéhez kell feltérképeznie:

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
private AddressDto address;
private int numPatients;
private String cityName;
}

Szolgáltatása lekéri a városok listáját és továbbítja a térképkészítőnek

@Service
public class DoctorService {

private final DoctorMapper doctorMapper;
private final DoctorRepository doctorRepository;

@Autowired
public DoctorService(DoctorMapper doctorMapper) {
this.doctorMapper = doctorMapper;
this.doctorRepository = doctorRepository;
}

public DoctorDto getDoctor(Integer id) {
Doctor doctor = doctorRepository.findById(id);
List cities = getCities();
return doctorMapper.toDto(doctor, cities);
}
}

Térképkészítőnkben:

  • meg kell jelölnünk a kiegészítő paramétert (városok listáját) @Context kommentárral
  • létrehozni egy egyéni módszer a leképezésünk kezelésére
  • kérje a kontextus paramétert (városok listáját) az egyedi ma-nkon pping módszer
@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
@Mapping(target = "numPatients", ignore = true)
@Mapping(source = "cityId",
target = "cityName",
qualifiedByName = "cityName")

DoctorDto toDto(Doctor doctor, @Context List cities);

@Named("cityName")
default String getCityName(int cityId, @Context List cities) {
return cities.stream()
.filter(city -> city.getId() == cityId)
.findAny()
.map(City::getName)
.orElse(null);
}

Függőség-injektálás a leképezési módszerekhez

Valószínűleg olyan helyzetekbe kerül, ahol az egyéni leképezési módszerek megkövetelik egy másik bab (másik leképező, adattár, szolgáltatás stb.).

Ezekben a helyzetekben ezt a babot automatikusan be kell írnia a térképezőjébe, ezért lássunk egy példát ennek végrehajtására.

Ebben a példában a beteg osztály absztrakt osztály lesz:

@Data
public abstract class Patient {
private int id;
private String name;
private int age;
}

Két megvalósítást tartalmaz:

public class Man extends Patient {
}public class Woman extends Patient {
}

Amint azt korábban láttuk, ez a Doctor entitás:

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
private Address address;
private List patients;
private int cityId;
}

Azonban a A PatientDto minden egyes konkrét osztályhoz külön listát igényel:

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
private AddressDto address;
private int numPatients;
private String cityName;
private PatientsDto patients;
}

@Data
public class PatientsDto {
private List men = new ArrayList();
private List women = new ArrayList();

public void addMan(ManDto manDto) {
men.add(manDto);
}

public void addWoman(WomanDto womanDto) {
women.add(womanDto);
}
}

Tehát ezeknek a konkrét osztályoknak a feltérképezéséhez el kell kezdenünk a g két leképező:

@Mapper(componentModel = "spring")
public interface WomanMapper {
WomanDto toDto(Woman woman);
}@Mapper(componentModel = "spring")
public interface ManMapper {
ManDto toDto(Man man);
}

De a

betegek – Betegek számára szükségünk van egy másik leképezőre is, amely az újonnan létrehozott térképeket használja ( WomanMapper és ManMapper ), amelyeket végül a DoctorMapper.

Mivel a PacientMapper használnunk kell a WomanMapper t és a ManMapper t, az interfész létrehozása helyett egy absztrakt osztályt kell létrehoznunk:

@Mapper(componentModel = "spring")
public abstract class PatientsMapper {

@Autowired
private ManMapper manMapper;

@Autowired
private WomanMapper womanMapper;

public PatientsDto toDto(List patients) {
PatientsDto patientsDto = new PatientsDto();
for (Patient patient : patients) {
if (patient instanceof Man) {
patientsDto.addMan(
manMapper.toDto((Man) patient));
} else if (patient instanceof Woman) {
patientsDto.addWoman(
womanMapper.toDto((Woman) patient));
}
}
return patientsDto;
}
}

Végül ahhoz, hogy DoctorMapper jünk felhasználhassa a BetegMapper t, hozzá kell adnunk néhány konfigurációt:

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {AddressMapper.class, PatientsMapper.class})
public interface DoctorMapper {
...
}

Mivel a betegek változónak ugyanaz a neve van az Entity és a Dto osztályokban, nem kell mást megadnunk.

Ez lesz a végeredmény a létrehozott osztályok közül:

@Component
public class DoctorMapperImpl implements DoctorMapper {

@Autowired
private AddressMapper addressMapper;
@Autowired
private PatientsMapper patientsMapper;


@Override
public DoctorDto toDto(Doctor doctor, List cities) {
if (doctor == null) {
return null;
}

DoctorDto doctorDto = new DoctorDto();

doctorDto.setNumPatients(
getNumPatients(doctor.getPatients()));
doctorDto.setCityName(
getCityName(doctor.getCityId(), cities));
doctorDto.setSpecialityName(doctorSpecialityName(doctor));
doctorDto.setContact(doctor.getPhone());
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
doctorDto.setAddress(
addressMapper.toDto(doctor.getAddress()));
doctorDto.setPatients(
patientsMapper.toDto(doctor.getPatients()));


return doctorDto;
}
...
}@Component
public class ManMapperImpl implements ManMapper {

@Override
public ManDto toDto(Man man) {
if ( man == null ) {
return null;
}

ManDto manDto = new ManDto();

manDto.setId( man.getId() );
manDto.setName( man.getName() );
manDto.setAge( man.getAge() );

return manDto;
}
}@Component
public class WomanMapperImpl implements WomanMapper {

@Override
public WomanDto toDto(Woman woman) {
if ( woman == null ) {
return null;
}

WomanDto womanDto = new WomanDto();

womanDto.setId( woman.getId() );
womanDto.setName( woman.getName() );
womanDto.setAge( woman.getAge() );

return womanDto;
}
}

Frissítések

A Mapstruct is biztosít s a frissítések kezelésének egyszerű módja. Ha frissíteni akarjuk az Doctor entitásunkat a DoctorDto , csak létre kell hoznunk:

@Mapping(source = "contact", target = "phone")
@Mapping(source = "specialityName", target = "speciality.name")
void updateEntity(DoctorDto doctorDto,
@MappingTarget Doctor doctor);

Amint a létrehozott megvalósításon láthatjuk, az összeset feltérképezi változók (null vagy nem null):

@Override
public void updateEntity(DoctorDto doctorDto, Doctor doctor) {
if ( doctorDto == null ) {
return;
}

if ( doctor.getSpeciality() == null ) {
doctor.setSpeciality( new Speciality() );
}
doctorDtoToSpeciality( doctorDto, doctor.getSpeciality() );
doctor.setPhone( doctorDto.getContact() );
doctor.setId( doctorDto.getId() );
doctor.setName( doctorDto.getName() );
if ( doctorDto.getAddress() != null ) {
if ( doctor.getAddress() == null ) {
doctor.setAddress( new Address() );
}
addressDtoToAddress( doctorDto.getAddress(), doctor.getAddress() );
}
else {
doctor.setAddress( null );
}
}

Frissítések javítása

Amint az előző példában láthattuk, az alapértelmezett frissítési módszer minden tulajdonságot feltérképez, még akkor is, ha az null.Tehát, ha olyan helyzetben találja magát, ahol csak javítást szeretne végrehajtani (csak a nem null értékeket frissítse), akkor a nullValuePropertyMappingStrategy t kell használnia:

@BeanMapping(nullValuePropertyMappingStrategy = 
NullValuePropertyMappingStrategy.IGNORE)

@Mapping(source = "contact", target = "phone")
@Mapping(source = "specialityName", target = "speciality.name")
void updatePatchEntity(DoctorDto doctorDto,
@MappingTarget Doctor doctor);

A létrehozott módszer null ellenőrzést hajt végre az értékek frissítése előtt:

@Override
public void updatePatchEntity(DoctorDto doctorDto, Doctor doctor) {
if ( doctorDto == null ) {
return;
}

if ( doctorDto.getContact() != null ) {
doctor.setPhone( doctorDto.getContact() );
}
doctor.setId( doctorDto.getId() );
if ( doctorDto.getName() != null ) {
doctor.setName( doctorDto.getName() );
}
if ( doctorDto.getAddress() != null ) {
if ( doctor.getAddress() == null ) {
doctor.setAddress( new Address() );
}
addressDtoToAddress(
doctorDto.getAddress(),
doctor.getAddress() );
}
if ( doctor.getSpeciality() == null ) {
doctor.setSpeciality( new Speciality() );
}
doctorDtoToSpeciality1( doctorDto, doctor.getSpeciality() );
}

Következtetés

Ez a cikk leírta, hogyan lehet kihasználni a Mapstruct könyvtárat, hogy biztonságos és elegáns módon jelentősen csökkentse a kazánlap kódját.

A példákból kitűnik, hogy a Mapstruct számos funkciót és konfigurációt kínál, amelyek lehetővé teszik számunkra, hogy alapvető és összetett térképezők számára egyszerű és gyors módon.