Sukella syvälle Mapstruct @ Springiin

Kuva: Beau Swierstra on Unsplash

Koska olet Java-kehittäjä, tiedät ehdottomasti, että POJOjen kartoitus on vakiotoiminto kehitettäessä monikerroksisia sovelluksia.
Näiden kartoitusten kirjoittaminen käsin paitsi tylsää ja tylsää epämiellyttävä tehtävä kehittäjille, on myös virhealtista.

MapStruct on avoimen lähdekoodin Java-kirjasto, joka luo kartoitinluokan toteutukset käännöksen aikana turvallinen ja helppo tapa.

Tämän artikkelin aikana seuraamme esimerkkiä siitä, miten hyödynnetään tätä tehokasta kirjastoa, jotta voidaan vähentää huomattavasti käsikirjoitettavien kattilakoodien määrää.

Hakemisto

  • Peruskartoitukset
  • Eri ominaisuuksien nimen kartoitus
  • Kaikkien ominaisuuksien kartoittaminen
  • Alakokonaisuudet-ominaisuuden kartoitus
  • Koko alatason kokonaisuuden kartoitus – Toisen kartoittimen käyttö
  • Kartoitus muokatulla menetelmällä
  • @BeforeMapping @AfterMapping
  • Kartoitus lisäparametrilla
  • Riippuvuussyöttö kartoitusmenetelmiin
  • Päivitykset
  • Korjauspäivitykset

Peruskartoitukset

Aloitetaan sovelluksemme perusmallilla, joka sisältää luokan Doctor. Palvelumme hakee tämän luokan mallikerrokselta ja palauttaa sitten DoctorDto luokka.

Malliluokka:

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

Dto-luokka:

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

Tätä varten meidän on luotava Mapper-käyttöliittymä:

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

Koska molemmilla luokilla on samat ominaisuusnimet ( id ja nimi ), mapstruct sisältää luodun luokan molempien kenttien kartoituksen:

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

Lisäämällä componentModel = “Spring” , luotu kartoitin on kevätpapu ja se voidaan hakea @Autowired -merkinnällä kuten kaikki muutkin pavut:

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

Eri ominaisuusnimien kartoitus

Jos sisällytämme ominaisuuden puhelin Doctor luokka:

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

Yhdistetään kontaktiin ryhmässä Lääkäri Dto :

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

Kartoittajamme ei pysty kartoittamaan sitä automaattisesti. Tätä varten meidän on luotava sääntö tälle kartoitukselle:

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

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

Varmista, että jokainen ominaisuus on kartoitettu

Jos haluamme Sen takaamiseksi, että emme unohda kartoittaa mitään kohdeominaisuutta, voimme määrittää unmappedTargetPolicy -vaihtoehdon kartoittimessamme:

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

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

Jos poistat tämän kokoonpanon,

@Mapping (source = ”phone”, target = ”contact”)

koodimme epäonnistuu käännettäessä virhe:

Kohdistamaton kohdeominaisuus: ”contact”. DoctorDto toDto (Doctor doctor);

Jos jostain syystä haluamme jättää kohteen huomiotta ominaisuus, voimme lisätä:

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

Vastaavasti voimme myös taata, että kaikki lähteet ominaisuudet kartoitetaan määrittämällä unmappedSourcePolicy -vaihtoehto.

Lapsikokonaisuuden ominaisuuden kartoitus

Suurimmaksi osaksi tarvitsemamme luokka kartoittaa sisältää lapsiobjekteja . Esimerkki:

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

Ja Dto: ssa tarvitsemme koko erikoisuuden sijaan vain sen nimen:

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

Tämä tilanne on suoraviivainen myös mapstruct in kanssa:

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

Koko lapsikokonaisuuden kartoitus – toisen käyttäminen mapper

Kuten aiemmin, lääkäri -luokassamme on lapsiobjekti Osoite :

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

Mutta tässä tapauksessa haluamme kartoittaa sen uuteen objekti DoctorDto -luokassa:

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

Suorita kartoitus Address- ja AddressDto-luokkien välillä, joten meidän on luotava erilainen mapper-käyttöliittymä:

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

Sitten meidän DoctorMapper meidän on varmistettava, että tätä kartoitinta käytetään kartoitettaessa Doctor – DoctorDto .Tämä voidaan tehdä kartoittimen kokoonpanon ” käyttää” -vaihtoehdolla:

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

Näemme, että DoctorMapperImpl käyttää Autowirea ja käyttää AddressMapper-sovellustamme :

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

...
}

Kartoitus mukautetulla menetelmällä

Lisätään nyt potilaiden luettelo Lääkäri luokka:

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

Kuitenkin DoctorDto haluamme vain potilaiden määrän:

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

Tämä kartoitus vaatii 2 asiat:

  • Mukautettu menetelmä @Nimi -merkinnällä
  • qualifiedByName -määritys Mapping-merkinnässä
@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

Edellinen esimerkki (kartoitus kohteesta Luettelo potilasta int numPotilaat ) voidaan tehdä myös @BeforeMapping ja @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());
}
}

Näitä menetelmiä kutsutaan luodun kartoitusmenetelmän alkuun ja loppuun:

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

Kartoitus lisäparametrilla

Tarkistetaan, miten käsitellään tilannetta, jossa kartoittajan on saatava Entiteetin lisäksi uusi parametri.

Tällöin lääkäriopettajasi voi saada vain kaupunkitunnuksen:

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

Mutta se on kartoitettava kaupungin nimeen:

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

Palvelusi noutaa luettelon kaupungeista ja välittää ne kartoittajalle

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

Kartoittimessamme meidän on:

  • merkittävä lisäparametri (luettelo kaupungeista) @Context -merkinnällä
  • luoda mukautettu menetelmä kartoituksen käsittelemiseksi
  • pyydä kontekstiparametriä (luettelo kaupungeista) mukautetussa maassamme hakutapa

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

Riippuvuussyöttö kartoitusmenetelmiin

Olet todennäköisesti tilanteissa, joissa mukautetut kartoitusmenetelmät edellyttävät toinen papu (toinen kartoittaja, arkisto, palvelu jne.).

Näissä tilanteissa sinun on kirjoitettava kyseinen papu automaattisesti kartoittajaasi, joten katsotaanpa esimerkki siitä, miten se tehdään.

Tässä esimerkissä Potilas -luokka on abstrakti luokka:

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

Sisältää kaksi toteutusta:

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

Kuten aiemmin näimme, tämä on tila Lääkäri -entiteetti:

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

PatientDto vaatii erillisen luettelon jokaiselle betoniluokalle:

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

Ja

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

Joten näiden konkreettisten luokkien kartoittamiseksi meidän pitäisi aloittaa luomisesta g kaksi kartoitinta:

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

Mutta kartoitetaan -luettelosta Potilaat Potilaille potilaille tarvitsemme myös toisen kartan, joka käyttää vasta luotuja kartoittimia ( WomanMapper ja ManMapper ), joita lopulta käyttää DoctorMapper.

Koska täytyy käyttää WomanMapper ja ManMapper , sen sijaan, että luodaan käyttöliittymä, meidän on luotava tiivistelmäluokka:

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

Jotta lopuksi saisimme DoctorMapper imme käyttämään Potilaskarttajaa , meidän on lisättävä kokoonpano:

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

Koska potilaat -muuttujalla on sama nimi Entity- ja Dto-luokissa, meidän ei tarvitse määrittää mitään muuta.

Tämä on lopputulos luoduista luokista:

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

Päivitykset

Mapstruct tarjoaa myös s helppo tapa käsitellä päivityksiä. Jos haluamme päivittää Doctor -entiteettimme DoctorDto , meidän on vain luotava:

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

Kuten näemme luotuista toteutuksista, se kartoittaa kaikki muuttujat (nolla tai ei nolla):

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

Korjauspäivitykset

Kuten edellisessä esimerkissä nähtiin, oletuspäivitystapa on kartoita kaikki kiinteistöt, vaikka ne olisivatkin tyhjiä.Joten jos kohtaat itsesi tilanteessa, jossa haluat vain päivittää korjaustiedoston (päivitä vain ei nolla-arvot), sinun on käytettävä nullValuePropertyMappingStrategy :

@BeanMapping(nullValuePropertyMappingStrategy = 
NullValuePropertyMappingStrategy.IGNORE)

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

Luotu menetelmä suorittaa tyhjät tarkistukset ennen arvojen päivittämistä:

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

Johtopäätös

Tämä artikkeli kuvasi kuinka hyödyntää Mapstruct-kirjastoa vähentääksemme kattilakoodiamme merkittävästi turvallisella ja tyylikkäällä tavalla.

Kuten esimerkeistä nähdään, Mapstruct tarjoaa laajan joukon toimintoja ja kokoonpanoja, joiden avulla voimme luoda peruskomponenteista monimutkaisiin kartoittajiin helposti ja nopeasti.