Tiefes Eintauchen in Mapstruct @ Spring

(Miguel Duque) (14. Dezember 2020)

Foto von Beau Swierstra auf Unsplash

Als Java-Entwickler wissen Sie definitiv, dass das Zuordnen von POJOs eine Standardaufgabe bei der Entwicklung mehrschichtiger Anwendungen ist.
Das manuelle Schreiben dieser Zuordnungen ist nicht nur langweilig und langweilig Eine unangenehme Aufgabe für Entwickler ist ebenfalls fehleranfällig.

MapStruct ist eine Open-Source-Java-Bibliothek, die während der Kompilierung Implementierungen von Mapper-Klassen generiert Ein sicherer und einfacher Weg.

In diesem Artikel werden wir einem Beispiel folgen, wie Sie diese leistungsstarke Bibliothek nutzen können, um die Menge an Boilerplate-Code, die regelmäßig von Hand geschrieben wird, erheblich zu reduzieren.

Index

  • Grundlegende Zuordnungen
  • Zuordnen verschiedener Eigenschaftsnamen
  • Sicherstellen, dass jede Eigenschaft zugeordnet ist
  • Zuordnung der untergeordneten Entitätseigenschaft
  • Zuordnung der vollständigen untergeordneten Entität – Verwenden einer anderen Zuordnung
  • Zuordnung mit einer benutzerdefinierten Methode
  • @BeforeMapping @AfterMapping
  • Mapping mit einem zusätzlichen Parameter
  • Abhängigkeitsinjektion von Mapping-Methoden
  • Updates
  • Patch-Updates

Grundlegende Mappings

Beginnen wir unsere Anwendung mit einem Grundmodell, das die Klasse Doctor enthält. Unser Service ruft diese Klasse aus der Modellebene ab und gibt dann einen 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;
}

Dazu sollten wir unsere Mapper-Schnittstelle erstellen:

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

Da beide Klassen dieselben Eigenschaftsnamen haben ( id em) > und name ) enthält mapstruct die Zuordnung beider Felder in der generierten Klasse:

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

Durch Hinzufügen von componentModel = „Spring“ , der generierte Mapper ist eine Spring-Bean und kann wie jede andere Bean mit der Annotation @Autowired abgerufen werden:

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

Zuordnen eines anderen Eigenschaftsnamens

Wenn wir eine Eigenschaft phone in den Doctor Klasse:

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

Wird in contact zugeordnet Doktor Dto :

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

Unser Mapper kann es nicht automatisch zuordnen. Dazu müssen wir eine Regel für diese Zuordnung erstellen:

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

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

Sicherstellen, dass jede Eigenschaft zugeordnet ist

Wenn wir möchten Um sicherzustellen, dass wir nicht vergessen, eine Zieleigenschaft zuzuordnen, können wir die Option unmappedTargetPolicy auf unserem Mapper konfigurieren:

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

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

Wenn wir bei dieser Konfiguration

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

entfernen, schlägt unser Code beim Kompilieren mit fehl der Fehler:

Nicht zugeordnete Zieleigenschaft: „contact“. DoctorDto toDto (Doctor doctor);

Wenn wir aus irgendeinem Grund ein Ziel ignorieren möchten Eigenschaft können wir hinzufügen:

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

Ebenso können wir auch garantieren, dass alle Quellen -Eigenschaften werden durch Konfigurieren der Option unmappedSourcePolicy zugeordnet.

Zuordnung der untergeordneten Entitätseigenschaft

Meistens die Klasse, die wir benötigen zu ordnen enthält untergeordnete Objekte . Zum Beispiel:

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

Und in unserem Dto benötigen wir anstelle der vollständigen Spezialität nur den Namen:

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

Diese Situation ist auch bei mapstruct unkompliziert:

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

Zuordnung der vollständigen untergeordneten Entität – Verwendung einer anderen Mapper

Wie zuvor hat unsere Doctor -Klasse ein untergeordnetes Objekt Adresse :

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

In diesem Fall möchten wir sie jedoch einer neuen zuordnen Objekt in unserer DoctorDto -Klasse:

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

To Wenn Sie die Zuordnung zwischen Address und AddressDto durchführen, sollten Sie eine andere Mapper-Schnittstelle erstellen:

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

Dann in unserer DoctorMapper Wir sollten sicherstellen, dass dieser Mapper verwendet wird, wenn der Doctor bis DoctorDto .Dies kann mit der Option „ using“ in der Mapper-Konfiguration erfolgen:

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

Wir werden sehen, dass unsere DoctorMapperImpl verdrahtet automatisch und verwendet unseren 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;
}

...
}

Zuordnung mit einer benutzerdefinierten Methode

Fügen wir jetzt eine Liste von Patienten zu unserer Doktor Klasse:

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

Auf unserer DoctorDto Wir möchten nur die Anzahl der Patienten:

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

Für diese Zuordnung sind 2 erforderlich Dinge:

  • Eine benutzerdefinierte Methode mit der Annotation @Named
  • Die Konfiguration qualifizierenByName in der Annotation Mapping
@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

Das vorherige Beispiel (Zuordnung von Patienten auflisten bis int numPatients ) kann auch mit @BeforeMapping em durchgeführt werden > und @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());
}
}

Diese Methoden werden am Anfang und Ende unserer generierten Zuordnungsmethode aufgerufen:

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

Zuordnung mit einem zusätzlichen Parameter

Überprüfen Sie, wie Sie mit einer Situation umgehen, in der Ihr Mapper neben Ihrer Entität einen zusätzlichen Parameter erhalten muss.

In diesem Fall kann Ihre Arztklasse nur die Stadt-ID abrufen:

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

Sie muss sie jedoch dem Stadtnamen zuordnen:

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

Ihr Dienst ruft die Liste der Städte ab und leitet sie an den Mapper weiter.

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

In unserem Mapper müssen wir:

  • den zusätzlichen Parameter (Liste der Städte) mit der Annotation @Context markieren
  • eine benutzerdefinierte erstellen Methode zur Behandlung unserer Zuordnung
  • fordern Sie den Kontextparameter (Liste der Städte) auf unserer benutzerdefinierten Ma an pping-Methode
@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);
}

Abhängigkeitsinjektion von Zuordnungsmethoden

Sie befinden sich wahrscheinlich in Situationen, in denen Ihre benutzerdefinierten Zuordnungsmethoden dies erfordern eine andere Bean (ein anderer Mapper, ein Repository, ein Dienst usw.).

In diesen Situationen müssen Sie diese Bean automatisch mit Ihrem Mapper verdrahten. Sehen wir uns also ein Beispiel dafür an.

In diesem Beispiel ist unsere Patientenklasse eine abstrakte Klasse:

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

Enthält zwei Implementierungen:

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

Wie wir zuvor gesehen haben, ist dies der Zustand unseres Doctor entity:

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

Unsere PatientDto erfordert eine eigene Liste für jede 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;
}

Und

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

Um diese konkreten Klassen abzubilden, sollten wir mit Creatin beginnen g zwei Mapper:

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

Zuordnen von Listen Sie -Patienten bis Patienten Für Patienten benötigen wir auch einen anderen Mapper, der die neu erstellten Mapper verwendet ( WomanMapper) und ManMapper ), die letztendlich von unserem DoctorMapper verwendet werden.

Da unser PatientsMapper dies tun wird Verwenden Sie WomanMapper und ManMapper , anstatt eine Schnittstelle zu erstellen, müssen Sie eine abstrakte Klasse erstellen:

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

Damit unser DoctorMapper den PatientsMapper verwendet, müssen wir einige Konfigurationen hinzufügen:

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

Da die Variable patient in den Klassen Entity und Dto denselben Namen hat, müssen wir nichts anderes angeben.

Dies ist das Endergebnis der generierten Klassen:

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

Updates

Mapstruct bieten ebenfalls Es ist eine einfache Möglichkeit, mit Updates umzugehen. Wenn wir unsere Entität Doctor mit den Informationen zu DoctorDto , wir müssen nur Folgendes erstellen:

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

Wie wir in der generierten Implementierung sehen können, werden alle zugeordnet Variablen (null oder nicht 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

Wie wir im vorherigen Beispiel gesehen haben, wird die Standard-Update-Methode verwendet Ordnen Sie jede Eigenschaft zu, auch wenn sie null ist.Wenn Sie also in eine Situation geraten, in der Sie nur ein Patch-Update durchführen möchten (nur die Nicht-Null-Werte aktualisieren), müssen Sie die nullValuePropertyMappingStrategy verwenden:

@BeanMapping(nullValuePropertyMappingStrategy = 
NullValuePropertyMappingStrategy.IGNORE)

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

Die generierte Methode führt vor dem Aktualisieren der Werte Nullprüfungen durch:

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

Schlussfolgerung

Dieser Artikel beschrieben, wie Sie die Mapstruct-Bibliothek nutzen können, um unseren Boilerplate-Code auf sichere und elegante Weise erheblich zu reduzieren.

Wie in den Beispielen gezeigt, bietet Mapstruct eine Vielzahl von Funktionen und Konfigurationen, aus denen wir erstellen können Grundlegende bis komplexe Mapper auf einfache und schnelle Weise.