Dyp dykk ned i Mapstruct @ Spring

(Miguel Duque) (14. desember 2020)

Foto av Beau Swierstra på Unsplash

Å være Java-utvikler, vet du absolutt at kartlegging av POJOer er en standard oppgave når du utvikler flerlagsapplikasjoner.
Å skrive disse kartleggingen manuelt, i tillegg til å være kjedelig ubehagelig oppgave for utviklere, er også feilutsatt.

MapStruct er et Java-bibliotek med åpen kildekode som genererer kartklassimplementeringer under kompilering i på en trygg og enkel måte.

I løpet av denne artikkelen vil vi følge et eksempel på hvordan du kan dra nytte av dette kraftige biblioteket for å redusere mengden kokerplatekode som regelmessig vil bli skrevet for hånd.

Indeks

  • Grunnleggende kartlegginger
  • Kartlegging av annet navn på eiendom
  • Sørge for at alle eiendommer blir kartlagt
  • Kartlegging av underordnet enhet
  • Kartlegging av hele underordnet enhet – Bruk en annen kartlegger
  • Kartlegging med en tilpasset metode
  • @BeforeMapping @AfterMapping
  • Kartlegging med en ekstra parameter
  • Avhengighetsinjeksjon av kartleggingsmetoder
  • Oppdateringer
  • Oppdateringer for oppdateringer

Grunnleggende kartlegginger

La oss starte søknaden vår med en grunnleggende modell, som inneholder klassen Doctor. Tjenesten vår vil hente denne klassen fra modellaget og deretter returnere en DoctorDto klasse.

Modellklasse:

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

Dto-klasse:

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

For å gjøre dette, bør vi opprette vårt Mapper-grensesnitt:

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

Ettersom begge klassene har samme eiendomsnavn ( id og navn ), vil mapstruct inkludere kartleggingen av begge feltene i den genererte klassen:

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

Ved å legge til componentModel = “Spring” , den genererte kartleggeren vil være en vårbønne og kan hentes med @Autowired kommentaren som alle andre bønner:

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

Kartlegging av annet navn på eiendom

Hvis vi inkluderer en eiendom telefon i Lege klasse:

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

Skal tilordnes til kontakt i Lege Dto :

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

Kartleggeren vår vil ikke kunne kartlegge den automatisk. For å gjøre dette må vi lage en regel for denne kartleggingen:

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

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

Sørg for at alle eiendommer er kartlagt

Hvis vi vil for å garantere at vi ikke glemmer å kartlegge noe måleiendom, kan vi konfigurere alternativet unmappedTargetPolicy på kartleggeren vår:

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

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

Hvis vi fjerner

@Mapping (source = «phone», target = «contact»)

med denne konfigurasjonen, mislykkes koden vår under kompilering med feilen:

Ikke-kartlagt målegenskap: “kontakt”. DoctorDto toDto (Doctor doctor);

Hvis vi av en eller annen grunn vil ignorere et mål eiendom kan vi legge til:

@Mapping (target = «contact», ignore = true)

På samme måte kan vi også garantere at alle kilder egenskaper kartlegges ved å konfigurere alternativet unmappedSourcePolicy .

Kartlegging av underordnet enhetsegenskap

Mesteparten av tiden, klassen vi trenger til kart inneholder underordnede objekter . For eksempel:

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

Og i vår Dto, i stedet for hele spesialiteten, trenger vi bare navnet:

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

Denne situasjonen er også grei med mapstruct :

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

Kartlegging av hele underordnet enhet – Bruk av en annen kartlegger

Som før har klassen vår et underobjekt Adresse :

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

Men i dette tilfellet vil vi kartlegge det til en ny objekt i vår DoctorDto klasse:

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

Til utføre kartleggingen mellom Adresse og AddressDto-klassen, bør vi opprette et annet kartleggergrensesnitt:

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

Så, i DoctorMapper vi bør sørge for at denne kartleggeren brukes når du kartlegger Doctor til DoctorDto .Dette kan gjøres med alternativet « bruker» på mapperkonfigurasjonen:

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

Vi vil se at vår DoctorMapperImpl vil Autowire og bruke 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;
}

...
}

Kartlegging med en tilpasset metode

La oss nå legge til en pasientliste i Lege klasse:

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

Imidlertid, på vår DoctorDto vi vil bare ha antall pasienter:

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

Denne kartleggingen krever 2 ting:

  • En egendefinert metode med @ Navnet merknaden
  • Konfigurasjonen kvalifisertBynavn på Kartleggingsnotatet
@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

Det forrige eksemplet (kartlegging fra Liste pasienter til int numPatients ) kan også gjøres med @BeforeMapping og @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());
}
}

Disse metodene vil bli kalt i begynnelsen og slutten av vår genererte kartleggingsmetode:

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

Kartlegging med en tilleggsparameter

La oss sjekke hvordan du håndterer en situasjon der kartleggeren din trenger å motta en ekstra parameter, i tillegg til enheten din.

I dette tilfellet kan legeklassen din bare få by-id:

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

Men må kartlegge den til bynavnet:

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

Tjenesten din vil hente listen over byer og overføre dem til kartleggeren

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

I kartleggeren vår må vi:

  • markere tilleggsparameteren (liste over byer) med @Context kommentar
  • lage en tilpasset metode for å håndtere kartleggingen vår
  • be om kontekstparameteren (liste over byer) på vår tilpassede ma pping-metode
@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);
}

Avhengighetsinjeksjon av kartleggingsmetoder

Du vil sannsynligvis befinne deg i situasjoner der dine tilpassede kartleggingsmetoder krever en annen bønne (en annen kartlegger, et lager, en tjeneste osv.).

I disse situasjonene må du Autowire den bønnen til kartleggeren din, så la oss se et eksempel på hvordan du gjør det.

I dette eksemplet vil Pasient -klassen være en abstrakt klasse:

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

Som inneholder to implementeringer:

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

Som vi tidligere så, er dette tilstanden til Lege enhet:

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

Imidlertid er vår PasientDto krever en distinkt liste for hver konkrete klasse:

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

Og

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

Så for å kartlegge disse konkrete klassene, bør vi starte med creatin g to kartleggere:

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

Men å kartlegge fra Liste pasienter til PasienterDtil pasienter trenger vi også en annen kartlegger som bruker de nyopprettede kartleggere ( WomanMapper og ManMapper ), som ender opp med å bli brukt av DoctorMapper.

Siden PatientsMapper vil trenger å bruke WomanMapper og ManMapper , i stedet for å opprette et grensesnitt, må vi lage en abstrakt klasse:

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

Til slutt, for å få vår DoctorMapper til å bruke PatientsMapper , må vi legge til noen konfigurasjoner:

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

Ettersom pasienter -variabelen har samme navn på klassen Entity og Dto, trenger vi ikke spesifisere noe annet.

Dette vil være sluttresultatet av de genererte klassene:

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

Oppdateringer

Mapstruct gir også er en enkel måte å håndtere oppdateringer på. Hvis vi vil oppdatere lege -enheten med informasjonen på DoctorDto , vi trenger bare å lage:

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

Som vi kan se på den genererte implementeringen, vil den kartlegge alt variabler (null eller ikke 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 );
}
}

Oppdateringer for oppdateringer

Som vi har sett i forrige eksempel, vil standard oppdateringsmetode kartlegg hver eiendom, selv om den er null.Så hvis du støter på deg selv i en situasjon der du bare vil utføre en oppdatering (bare oppdater ikke nullverdiene), må du bruke nullValuePropertyMappingStrategy :

@BeanMapping(nullValuePropertyMappingStrategy = 
NullValuePropertyMappingStrategy.IGNORE)

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

Den genererte metoden utfører nullkontroller før verdiene oppdateres:

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

Konklusjon

Denne artikkelen beskrev hvordan vi kan dra nytte av Mapstruct-biblioteket for å redusere kokerplatekoden vår på en trygg og elegant måte.

Som vist i eksemplene, tilbyr Mapstruct et stort sett med funksjonaliteter og konfigurasjoner som lar oss lage fra grunnleggende til komplekse kartleggere på en enkel og rask måte.