Hluboký ponor do Mapstruct @ Spring

(Miguel Duque) (14. prosince 2020)

Foto: Beau Swierstra na Unsplash

Jelikož jste vývojář Java, určitě víte, že mapování POJO je standardní úkol při vývoji vícevrstvých aplikací.
Ruční psaní těchto mapování, kromě toho, že je to nudné a nepříjemný úkol pro vývojáře, je také náchylný k chybám.

MapStruct je open-source knihovna Java, která generuje implementace mapovacích tříd během kompilace v bezpečný a snadný způsob.

V tomto článku budeme následovat příklad, jak využít této výkonné knihovny k výraznému snížení množství standardního kódu, který by se pravidelně psal ručně.

Rejstřík

  • Základní mapování
  • Mapování různých názvů vlastností
  • Zajištění mapování každé vlastnosti
  • Mapování vlastnosti podřízené entity
  • Mapování celé podřízené entity – použití jiného mapovače
  • Mapování pomocí vlastní metody
  • @BeforeMapping @AfterMapping
  • Mapování s dalším parametrem
  • Vložení závislosti na metodách mapování
  • Aktualizace
  • Aktualizace oprav

Základní mapování

Začněme naši aplikaci základním modelem, který obsahuje třídu Doctor. Naše služba načte tuto třídu z vrstvy modelu a poté vrátí DoctorDto třída.

Třída modelu:

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

Třída Dto:

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

Chcete-li to provést, měli bychom vytvořit naše rozhraní Mapper:

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

Protože obě třídy mají stejné názvy vlastností ( id a name ), mapstruct zahrne mapování obou polí do generované třídy:

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

Přidáním componentModel = “Jaro” , vygenerovaný mapovač bude jarní fazole a lze ji načíst pomocí poznámky @Autowired jako každá jiná fazole:

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

Mapování různých názvů nemovitostí

Pokud do doktora iv id = zahrneme vlastnost telefon „ba87a1cd20“> třída:

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

Bude mapováno na kontakt v Doktore Dto :

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

Náš mapovač to nebude moci mapovat automaticky. K tomu budeme muset vytvořit pravidlo pro toto mapování:

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

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

Zajistit, aby byla mapována každá vlastnost

Pokud chceme abychom zajistili, že nezapomeneme mapovat žádnou cílovou vlastnost, můžeme nakonfigurovat možnost unmappedTargetPolicy na našem mapovači:

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

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

Pokud v této konfiguraci odstraníme

@Mapping (source = „phone“, target = „contact“)

náš kód selže během kompilace s chyba:

Nezmapovaná vlastnost cíle: „kontakt“. DoctorDto toDto (lékař);

Pokud z nějakého důvodu chceme cíl ignorovat vlastnost, můžeme přidat:

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

Podobně můžeme také zaručit, že všechny zdroje vlastnosti jsou mapovány konfigurací možnosti unmappedSourcePolicy .

Mapování vlastnosti podřízené entity

Většinu času potřebujeme třídu mapovat obsahuje podřízené objekty . Například:

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

A v našem Dto potřebujeme místo celé specializace pouze jeho název:

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

Tato situace je také přímá s mapstruct :

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

Mapování celé podřízené entity – použití jiné mapovač

Stejně jako dříve má naše třída Doctor podřízený objekt Adresa :

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

V tomto případě jej však chceme namapovat na nový objekt v naší DoctorDto třídě:

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

provést mapování mezi třídou Address a AddressDto, měli bychom vytvořit jiné rozhraní mapovače:

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

Potom v našem DoctorMapper měli bychom se ujistit, že tento mapovač je použit při mapování Doctor to DoctorDto .To lze provést pomocí možnosti „ použití“ v konfiguraci mapovače:

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

Uvidíme, že naše DoctorMapperImpl provede autowire a použije naši 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;
}

...
}

Mapování pomocí vlastní metody

Pojďme nyní přidat seznam pacientů do naší Doktor třída:

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

Na naší DoctorDto chceme pouze počet pacientů:

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

Toto mapování vyžaduje 2 things:

  • Vlastní metoda s anotací @Named
  • Konfigurace kvalifikovanýByName v anotaci Mapování
@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

Předchozí příklad (mapování z Seznam pacientů int numPatients ) lze provést také pomocí @BeforeMapping and @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());
}
}

Tyto metody budou volány na začátku a na konci naší generované metody mapování:

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

Mapování s dalším parametrem

Pojďme zkontrolovat, jak řešit situaci, kdy váš mapovač potřebuje kromě vaší entity získat další parametr.

V tomto případě může vaše doktorská třída získat pouze ID města:

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

Je ale třeba jej namapovat na Název města:

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

Vaše služba načte seznam měst a předá je mapovači

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

V našem mapovači musíme:

  • označit další parametr (seznam měst) @Context anotací
  • vytvořit vlastní metoda pro zpracování našeho mapování
  • vyžádejte si kontextový parametr (seznam měst) na naší vlastní ma metoda pping

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

Vložení závislosti na metodách mapování

Pravděpodobně se ocitnete v situacích, kdy to vyžadují vaše vlastní metody mapování jiný fazole (jiný mapovač, úložiště, služba atd.).

V těchto situacích budete muset tento fazole autowire dát svému mapovači, takže si prohlédněte příklad, jak to udělat.

V tomto příkladu bude naše třída Pacient abstraktní třída:

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

Která obsahuje dvě implementace:

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

Jak jsme viděli dříve, toto je stav našeho Doktor entita:

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

Naše PatientDto vyžaduje pro každou konkrétní třídu samostatný seznam:

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

A

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

Abychom tedy zmapovali tyto konkrétní třídy, měli bychom začít kreatinem g dva mapovače:

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

Ale k mapování od Seznam pacientů to PacientiDo pacientů potřebujeme také jiného mapovače, který používá nově vytvořené mapovače ( WomanMapper a ManMapper ), které nakonec použije náš DoctorMapper.

Protože naše PacientiMapper je třeba použít WomanMapper a ManMapper , místo vytvoření rozhraní musíme vytvořit třídu Abstract:

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

Nakonec, aby náš DoctorMapper používal PacientiMapper , musíme přidat nějakou konfiguraci:

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

Protože proměnná pacienti má ve třídách Entity a Dto stejný název, není třeba nic dalšího specifikovat.

Toto bude konečný výsledek generovaných tříd:

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

Aktualizace

Mapstruct také poskytuje je snadný způsob zpracování aktualizací. Pokud chceme aktualizovat naši Doctor entitu o informace o DoctorDto , stačí vytvořit:

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

Jak vidíme na generované implementaci, bude mapovat všechny proměnné (null or not 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 );
}
}

Patch Updates

Jak jsme viděli v předchozím příkladu, výchozí metoda aktualizace bude mapovat každou vlastnost, i když je nulová.Pokud se tedy setkáte se situací, kdy chcete provést aktualizaci patche (aktualizujte pouze hodnoty null), musíte použít nullValuePropertyMappingStrategy :

@BeanMapping(nullValuePropertyMappingStrategy = 
NullValuePropertyMappingStrategy.IGNORE)

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

Vygenerovaná metoda provede před aktualizací hodnot nulové kontroly:

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

Závěr

Tento článek popsal, jak využít knihovnu Mapstruct k bezpečnému a elegantnímu snížení našeho standardizovaného kódu.

Jak je vidět v příkladech, Mapstruct nabízí širokou škálu funkcí a konfigurací, které nám umožňují vytvářet z základní a složité mapovače jednoduchým a rychlým způsobem.