Mapstruct @Springの詳細

Miguel Duque)(2020年12月14日)

Unsplashの Beau Swierstra による写真 figcaption>

Java開発者であるあなたは、POJOのマッピングが多層アプリケーションを開発する際の標準的なタスクであることを確実に知っています。
これらのマッピングを手動で書くことは、退屈で開発者にとって不快なタスクであり、エラーが発生しやすくなります。

MapStruct は、コンパイル中にマッパークラスの実装を生成するオープンソースのJavaライブラリです。安全で簡単な方法。

この記事では、この強力なライブラリを利用して、通常は手作業で作成される定型コードの量を大幅に削減する方法の例を示します。

インデックス

  • 基本的なマッピング
  • 異なるプロパティ名のマッピング
  • すべてのプロパティがマッピングされていることを確認する
  • 子エンティティプロパティをマッピングする
  • 完全な子エンティティをマッピングする—別のマッパーを使用する
  • カスタムメソッドを使用したマッピング
  • @BeforeMapping @AfterMapping
  • 追加のパラメーターを使用したマッピング
  • マッピングメソッドへの依存関係の注入
  • 更新
  • パッチの更新

基本的なマッピング

Doctorクラスを含む基本モデルからアプリケーションを開始しましょう。 サービスはモデルレイヤーからこのクラスを取得し、 DoctorDto クラス。

モデルクラス:

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

Dtoクラス:

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

これを行うには、マッパーインターフェイスを作成する必要があります:

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

両方のクラスのプロパティ名が同じであるため( id em および name )の場合、mapstructには、生成されたクラスの両方のフィールドのマッピングが含まれます。

@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を追加することにより=“ spring” 、生成されたマッパーはSpring Beanであり、他のBeanと同様に @Autowired アノテーションを使用して取得できます:

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

異なるプロパティ名のマッピング

医師ivid =にプロパティ phone を含める場合”ba87a1cd20″> クラス:

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

連絡先にマッピングされます医師Dto

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

マッパーは自動的にマッピングできません。これを行うには、このマッピングのルールを作成する必要があります。

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

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

すべてのプロパティがマップされていることを確認する

必要に応じてターゲットプロパティをマッピングすることを忘れないようにするために、マッパーで unmappedTargetPolicy オプションを構成できます。

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

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

この構成では、削除すると

@Mapping(source = “phone”、target = “contact”)

コードはコンパイル中に失敗します。エラー:

マップされていないターゲットプロパティ:「contact」。DoctorDtotoDto(Doctor doctor);

何らかの理由で、ターゲットを無視したい場合プロパティに追加できます:

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

同様に、すべてのソースを保証することもできます プロパティは、 unmappedSourcePolicy オプションを構成することでマッピングされます。

子エンティティプロパティのマッピング

ほとんどの場合、必要なクラスマップするには子オブジェクトが含まれます。例:

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

Dtoでは、完全な専門分野ではなく、名前だけが必要です:

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

この状況は、 mapstruct でも簡単です:

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

完全な子エンティティのマッピング—別のエンティティの使用mapper

以前と同様に、 Doctor クラスには子オブジェクトアドレス

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

ただし、この場合は、新しいアドレスにマッピングします DoctorDto クラスのオブジェクト:

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

To AddressクラスとAddressDtoクラスの間のマッピングを実行するには、別のマッパーインターフェイスを作成する必要があります。

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

次に、 DoctorMapper Doctor から DoctorDto 。これは、マッパー構成の「 uses」オプションを使用して実行できます。

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

DoctorMapperImpl は、 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;
}

...
}

カスタムメソッドを使用したマッピング

次に、患者のリストを医師クラス:

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

ただし、 DoctorDto 必要なのは患者数のみです:

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

このマッピングには2つ必要です事柄:

  • @Named アノテーションを使用したカスタムメソッド
  • Mappingアノテーションの qualifiedByName 構成
@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

前の例(リスト患者から int numPatients )は、 @BeforeMapping emでも実行できます。 >および @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());
}
}

これらのメソッドは、生成されたマッピングメソッドの最初と最後で呼び出されます。

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

追加のパラメーターを使用したマッピング

エンティティ以外に、マッパーが追加のパラメーターを受け取る必要がある状況を処理する方法を確認しましょう。

この場合、Doctorクラスは都市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;
}

ただし、都市名にマップする必要があります:

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

サービスは都市のリストを取得し、それらをマッパーに渡します

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

マッパーでは、次のことを行う必要があります。

  • 追加のパラメーター(都市のリスト)に @Context アノテーションを付ける
  • カスタムを作成するマッピングを処理するメソッド
  • カスタムmaのコンテキストパラメータ(都市のリスト)をリクエストします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);
}

マッピングメソッドへの依存性注入

カスタムマッピングメソッドが必要な状況に遭遇する可能性があります別のBean(別のマッパー、リポジトリ、サービスなど)。

このような状況では、そのBeanをマッパーに自動配線する必要があるため、その方法の例を見てみましょう。

この例では、 Patient クラスは抽象クラスになります。

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

これには2つの実装が含まれています:

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

前に見たように、これは医師エンティティ:

@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 には、具象クラスごとに個別のリストが必要です:

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

したがって、これらの具象クラスをマッピングするには、creatinから始める必要があります。 g 2つのマッパー:

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

ただし、リスト患者からマップするには to PatientsDtopatients また、新しく作成されたマッパーを使用する別のマッパー( WomanMapper および ManMapper )、これは最終的に DoctorMapperによって使用されます。

PatientsMapper は、インターフェイスを作成する代わりに、 WomanMapper ManMapper を使用する必要があります。抽象クラスを作成する必要があります:

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

最後に、 DoctorMapper PatientsMapper を使用するようにするには、いくつかの構成を追加する必要があります。

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

patients 変数はEntityクラスとDtoクラスで同じ名前であるため、他に何も指定する必要はありません。

これが最終結果になります生成されたクラスの例:

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

更新

Mapstructも提供します■更新を処理する簡単な方法。 Doctor エンティティを DoctorDto 、作成する必要があります:

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

生成された実装でわかるように、すべてがマップされます変数(nullまたは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 );
}
}

パッチ更新

前の例で見たように、デフォルトの更新方法はnullであっても、すべてのプロパティをマップします。したがって、パッチの更新(null以外の値のみを更新する)を実行したい状況に遭遇した場合は、 nullValuePropertyMappingStrategy を使用する必要があります:

@BeanMapping(nullValuePropertyMappingStrategy = 
NullValuePropertyMappingStrategy.IGNORE)

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

生成されたメソッドは、値を更新する前にnullチェックを実行します:

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

結論

この記事 Mapstructライブラリを利用して、ボイラープレートコードを安全かつエレガントな方法で大幅に削減する方法について説明しました。

例に示されているように、Mapstructは、以下から作成できる機能と構成の膨大なセットを提供します。 基本的なマッパーから複雑なマッパーまで、簡単かつ迅速に。