MapStructのクイックガイド

MapStructのクイックガイド

1. 概要

この記事では、MapStructの使用について説明します。これは、簡単に言えば、JavaBeanマッパーです。

このAPIには、2つのJava Bean間を自動的にマップする関数が含まれています。 MapStructを使用すると、インターフェイスを作成するだけで済み、ライブラリはコンパイル時に自動的に具体的な実装を作成します。

2. MapStructとTransferObject Pattern

ほとんどのアプリケーションでは、POJOを他のPOJOに変換する多くの定型コードがあります。

たとえば、一般的なタイプの変換は、永続性をサポートするエンティティとクライアント側に送信されるDTOの間で発生します。

これが、MapStructがを手動で解決するという問題であり、Beanマッパーの作成には時間がかかります。 ライブラリcan generate bean mapper classes automatically

3. メーベン

以下の依存関係をMavenpom.xmlに追加しましょう:


    org.mapstruct
    mapstruct-jdk8
    1.3.0.Beta2

Mapstructと彼のprocessorの最新の安定版リリースは、どちらもMaven中央リポジトリーから入手できます。

また、maven-compiler-pluginプラグインの構成部分にannotationProcessorPathsセクションを追加しましょう。

mapstruct-processorは、ビルド中にマッパー実装を生成するために使用されます。


    org.apache.maven.plugins
    maven-compiler-plugin
    3.5.1
    
        1.8
        1.8
        
            
                org.mapstruct
                mapstruct-processor
                1.3.0.Beta2
            
        
    

4. 基本的なマッピング

4.1. POJOの作成

最初に簡単なJava POJOを作成しましょう:

public class SimpleSource {
    private String name;
    private String description;
    // getters and setters
}

public class SimpleDestination {
    private String name;
    private String description;
    // getters and setters
}

4.2. マッパーインターフェイス

@Mapper
public interface SimpleSourceDestinationMapper {
    SimpleDestination sourceToDestination(SimpleSource source);
    SimpleSource destinationToSource(SimpleDestination destination);
}

MapStructが実装クラスを作成するため、SimpleSourceDestinationMapperの実装クラスを作成しなかったことに注意してください。

4.3. 新しいマッパー

mvn clean installを実行することで、MapStruct処理をトリガーできます。

これにより、/target/generated-sources/annotations/の下に実装クラスが生成されます。

MapStructが自動作成するクラスは次のとおりです。

public class SimpleSourceDestinationMapperImpl
  implements SimpleSourceDestinationMapper {
    @Override
    public SimpleDestination sourceToDestination(SimpleSource source) {
        if ( source == null ) {
            return null;
        }
        SimpleDestination simpleDestination = new SimpleDestination();
        simpleDestination.setName( source.getName() );
        simpleDestination.setDescription( source.getDescription() );
        return simpleDestination;
    }
    @Override
    public SimpleSource destinationToSource(SimpleDestination destination){
        if ( destination == null ) {
            return null;
        }
        SimpleSource simpleSource = new SimpleSource();
        simpleSource.setName( destination.getName() );
        simpleSource.setDescription( destination.getDescription() );
        return simpleSource;
    }
}

4.4. テストケース

最後に、すべてが生成されたら、SimpleSourceの値がSimpleDestinationの値と一致することを示すテストケースを作成しましょう。

public class SimpleSourceDestinationMapperIntegrationTest {
    private SimpleSourceDestinationMapper mapper
      = Mappers.getMapper(SimpleSourceDestinationMapper.class);
    @Test
    public void givenSourceToDestination_whenMaps_thenCorrect() {
        SimpleSource simpleSource = new SimpleSource();
        simpleSource.setName("SourceName");
        simpleSource.setDescription("SourceDescription");
        SimpleDestination destination = mapper.sourceToDestination(simpleSource);

        assertEquals(simpleSource.getName(), destination.getName());
        assertEquals(simpleSource.getDescription(),
          destination.getDescription());
    }
    @Test
    public void givenDestinationToSource_whenMaps_thenCorrect() {
        SimpleDestination destination = new SimpleDestination();
        destination.setName("DestinationName");
        destination.setDescription("DestinationDescription");
        SimpleSource source = mapper.destinationToSource(destination);
        assertEquals(destination.getName(), source.getName());
        assertEquals(destination.getDescription(),
          source.getDescription());
    }
}

5. 依存性注入によるマッピング

次に、Mappers.getMapper(YourClass.class).を呼び出すだけで、MapStructでマッパーのインスタンスを取得しましょう。

もちろん、これはインスタンスを取得する非常に手動の方法です。はるかに優れた代替策は、マッパーを必要な場所に直接注入することです(プロジェクトで依存性注入ソリューションを使用する場合)。

Luckily MapStruct has solid support for both Spring and CDIContexts and Dependency Injection)。

マッパーでSpringIoCを使用するには、componentModel属性を@Mapperに値springで追加する必要があり、CDIの場合はcdiになります。

5.1. マッパーを変更する

次のコードをSimpleSourceDestinationMapperに追加します。

@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper

6. 異なるフィールド名を持つフィールドのマッピング

前の例から、MapStructは同じフィールド名を持つため、Beanを自動的にマッピングできました。 では、マッピングしようとしているBeanのフィールド名が異なる場合はどうでしょうか?

この例では、EmployeeおよびEmployeeDTOという名前の新しいBeanを作成します。

6.1. 新しいPOJO

public class EmployeeDTO {
    private int employeeId;
    private String employeeName;
    // getters and setters
}
public class Employee {
    private int id;
    private String name;
    // getters and setters
}

6.2. マッパーインターフェイス

異なるフィールド名をマッピングする場合は、ソースフィールドをターゲットフィールドに構成する必要があります。そのためには、@Mappingsアノテーションを追加する必要があります。 このアノテーションは、ターゲット属性とソース属性を追加するために使用する@Mappingアノテーションの配列を受け入れます。

MapStructでは、ドット表記を使用してBeanのメンバーを定義することもできます。

@Mapper
public interface EmployeeMapper {
    @Mappings({
      @Mapping(target="employeeId", source="entity.id"),
      @Mapping(target="employeeName", source="entity.name")
    })
    EmployeeDTO employeeToEmployeeDTO(Employee entity);
    @Mappings({
      @Mapping(target="id", source="dto.employeeId"),
      @Mapping(target="name", source="dto.employeeName")
    })
    Employee employeeDTOtoEmployee(EmployeeDTO dto);
}

6.3. テストケース

繰り返しますが、ソースと宛先の両方のオブジェクトの値が一致することをテストする必要があります。

@Test
public void givenEmployeeDTOwithDiffNametoEmployee_whenMaps_thenCorrect() {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setEmployeeId(1);
    dto.setEmployeeName("John");

    Employee entity = mapper.employeeDTOtoEmployee(dto);

    assertEquals(dto.getEmployeeId(), entity.getId());
    assertEquals(dto.getEmployeeName(), entity.getName());
}

より多くのテストケースがGithub projectにあります。

7. Beanと子Beanのマッピング

次に、他のBeanへの参照を使用してBeanをマップする方法を示します。

7.1. POJOを変更する

Employeeオブジェクトに新しいBean参照を追加しましょう。

public class EmployeeDTO {
    private int employeeId;
    private String employeeName;
    private DivisionDTO division;
    // getters and setters omitted
}
public class Employee {
    private int id;
    private String name;
    private Division division;
    // getters and setters omitted
}
public class Division {
    private int id;
    private String name;
    // default constructor, getters and setters omitted
}

7.2. マッパーを変更する

ここで、DivisionDivisionDTOに、またはその逆に変換するメソッドを追加する必要があります。 MapStructは、オブジェクトタイプを変換する必要があることを検出し、変換するメソッドが同じクラスに存在する場合、それを自動的に使用します。

これをマッパーに追加しましょう:

DivisionDTO divisionToDivisionDTO(Division entity);

Division divisionDTOtoDivision(DivisionDTO dto);

7.3. テストケースを変更する

いくつかのテストケースを変更して、既存のテストケースに追加しましょう。

@Test
public void givenEmpDTONestedMappingToEmp_whenMaps_thenCorrect() {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setDivision(new DivisionDTO(1, "Division1"));
    Employee entity = mapper.employeeDTOtoEmployee(dto);
    assertEquals(dto.getDivision().getId(),
      entity.getDivision().getId());
    assertEquals(dto.getDivision().getName(),
      entity.getDivision().getName());
}

8. 型変換によるマッピング

MapStructは、いくつかの既成の暗黙的な型変換も提供します。この例では、文字列の日付を実際のDateオブジェクトに変換しようとします。

暗黙的な型変換の詳細については、MapStruct reference guideをお読みください。

8.1. Beansを変更する

従業員の開始日を追加します。

public class Employee {
    // other fields
    private Date startDt;
    // getters and setters
}
public class EmployeeDTO {
    // other fields
    private String employeeStartDt;
    // getters and setters
}

8.2. マッパーを変更する

マッパーを変更し、開始日のdateFormatを指定します。

@Mappings({
  @Mapping(target="employeeId", source = "entity.id"),
  @Mapping(target="employeeName", source = "entity.name"),
  @Mapping(target="employeeStartDt", source = "entity.startDt",
           dateFormat = "dd-MM-yyyy HH:mm:ss")})
EmployeeDTO employeeToEmployeeDTO(Employee entity);
@Mappings({
  @Mapping(target="id", source="dto.employeeId"),
  @Mapping(target="name", source="dto.employeeName"),
  @Mapping(target="startDt", source="dto.employeeStartDt",
           dateFormat="dd-MM-yyyy HH:mm:ss")})
Employee employeeDTOtoEmployee(EmployeeDTO dto);

8.3. テストケースを変更する

さらにいくつかのテストケースを追加して、変換が正しいことを確認します。

private static final String DATE_FORMAT = "dd-MM-yyyy HH:mm:ss";
@Test
public void givenEmpStartDtMappingToEmpDTO_whenMaps_thenCorrect() throws ParseException {
    Employee entity = new Employee();
    entity.setStartDt(new Date());
    EmployeeDTO dto = mapper.employeeToEmployeeDTO(entity);
    SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);

    assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
      entity.getStartDt().toString());
}
@Test
public void givenEmpDTOStartDtMappingToEmp_whenMaps_thenCorrect() throws ParseException {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setEmployeeStartDt("01-04-2016 01:00:00");
    Employee entity = mapper.employeeDTOtoEmployee(dto);
    SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);

    assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
      entity.getStartDt().toString());
}

9. 抽象クラスを使用したマッピング

@Mapping capabilitiesを超える方法でマッパーをカスタマイズしたい場合があります。

たとえば、型変換に加えて、次の例のように値を何らかの方法で変換することもできます。

そのような場合、抽象クラスを作成し、カスタマイズしたいメソッドを実装し、MapStructによって生成されるメソッドを抽象化することができます。

9.1. 基本モデル

この例では、次のクラスを使用します。

public class Transaction {
    private Long id;
    private String uuid = UUID.randomUUID().toString();
    private BigDecimal total;

    //standard getters
}

一致するDTO:

public class TransactionDTO {

    private String uuid;
    private Long totalInCents;

    // standard getters and setters
}

ここで注意が必要なのは、BigDecimaltotalの金額をLong totalInCentsに変換することです。

9.2. マッパーの定義

これは、Mapper を抽象クラスとして作成することで実現できます。

@Mapper
abstract class TransactionMapper {

    public TransactionDTO toTransactionDTO(Transaction transaction) {
        TransactionDTO transactionDTO = new TransactionDTO();
        transactionDTO.setUuid(transaction.getUuid());
        transactionDTO.setTotalInCents(transaction.getTotal()
          .multiply(new BigDecimal("100")).longValue());
        return transactionDTO;
    }

    public abstract List toTransactionDTO(
      Collection transactions);
}

ここでは、単一オブジェクト変換用に完全にカスタマイズされたマッピング方法を実装しました。

一方、CollectionListabstractにマップすることを目的としたメソッドを残したので、MapStruct がそれを実装します。

9.3. 生成された結果

単一のTransactionTransactionDTOにマップするメソッドをすでに実装しているため、Mapstructが2番目のメソッドでそれを使用することを期待します。 以下が生成されます:

@Generated
class TransactionMapperImpl extends TransactionMapper {

    @Override
    public List toTransactionDTO(Collection transactions) {
        if ( transactions == null ) {
            return null;
        }

        List list = new ArrayList<>();
        for ( Transaction transaction : transactions ) {
            list.add( toTransactionDTO( transaction ) );
        }

        return list;
    }
}

12行目にあるように、MapStruct は、生成されたメソッドの実装を使用します。

10. マッピング前およびマッピング後の注釈

@BeforeMappingおよび@AfterMappingアノテーションを使用して、@Mapping機能をカスタマイズする別の方法を次に示します。 The annotations are used to mark methods that are invoked right before and after the mapping logic

これらは、このbehavior to be applied to all mapped super-typesが必要になる可能性があるシナリオで非常に役立ちます。

Car;ElectricCar,およびBioDieselCarのサブタイプをCarDTOにマップする例を見てみましょう。

マッピング中に、型の概念をDTOのFuelType列挙型フィールドにマッピングし、マッピングが完了したら、DTOの名前を大文字に変更します。

10.1. 基本モデル

この例では、次のクラスを使用します。

public class Car {
    private int id;
    private String name;
}

Carのサブタイプ:

public class BioDieselCar extends Car {
}
public class ElectricCar extends Car {
}

列挙型フィールドタイプがFuelTypeCarDTO

public class CarDTO {
    private int id;
    private String name;
    private FuelType fuelType;
}
public enum FuelType {
    ELECTRIC, BIO_DIESEL
}
10.2. マッパーの定義

次に、CarCarDTOにマップする抽象マッパークラスを作成しましょう。

@Mapper
public abstract class CarsMapper {
    @BeforeMapping
    protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDto) {
        if (car instanceof ElectricCar) {
            carDto.setFuelType(FuelType.ELECTRIC);
        }
        if (car instanceof BioDieselCar) {
            carDto.setFuelType(FuelType.BIO_DIESEL);
        }
    }

    @AfterMapping
    protected void convertNameToUpperCase(@MappingTarget CarDTO carDto) {
        carDto.setName(carDto.getName().toUpperCase());
    }

    public abstract CarDTO toCarDto(Car car);
}

@MappingTargetは、populates the target mapping DTO right before the mapping logic is executedin case of @BeforeMappingおよびright after in case of @AfterMappingアノテーション付きメソッドのパラメーターアノテーションです。

10.3. 結果

CarsMapper defined above generatestheimplementation

@Generated
public class CarsMapperImpl extends CarsMapper {

    @Override
    public CarDTO toCarDto(Car car) {
        if (car == null) {
            return null;
        }

        CarDTO carDTO = new CarDTO();

        enrichDTOWithFuelType(car, carDTO);

        carDTO.setId(car.getId());
        carDTO.setName(car.getName());

        convertNameToUpperCase(carDTO);

        return carDTO;
    }
}

実装のthe annotated methods invocations surround the mapping logicに注目してください。

11. Lombokのサポート

MapStructの最新バージョンでは、Lombokのサポートが発表されました。 So we can easily map a source entity and a destination using Lombok. 

Lombokサポートを有効にするには、注釈プロセッサパスにthe dependencyを追加する必要があります。 したがって、Mavenコンパイラプラグインには、mapstruct-processorとLombokがあります。


    org.apache.maven.plugins
    maven-compiler-plugin
    3.5.1
    
        1.8
        1.8
        
            
                org.mapstruct
                mapstruct-processor
                1.3.0.Beta2
            
            
                org.projectlombok
                lombok
            1.18.4
            
        
    

Lombokアノテーションを使用してソースエンティティを定義しましょう。

@Getter
@Setter
public class Car {
    private int id;
    private String name;
}

そして、宛先データ転送オブジェクト:

@Getter
@Setter
public class CarDTO {
    private int id;
    private String name;
}

このためのマッパーインターフェイスは、前の例と同様のままです。

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    CarDTO carToCarDTO(Car car);
}

12. defaultExpressionのサポート

バージョン1.3.0以降、we can use the defaultExpression attribute of the @Mapping annotation to specify an expression that determines the value of the destination field if the source field is null.これは既存のdefaultValue属性機能に追加されます。

ソースエンティティ:

public class Person {
    private int id;
    private String name;
}

宛先データ転送オブジェクト:

public class PersonDTO {
    private int id;
    private String name;
}

ソースエンティティのidフィールドがnull,の場合、ランダムなidを生成し、他のプロパティ値をそのままにして宛先に割り当てます。

@Mapper
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(target = "id", source = "person.id",
      defaultExpression = "java(java.util.UUID.randomUUID().toString())")
    PersonDTO personToPersonDTO(Person person);
}

式の実行を検証するためのテストケースを追加しましょう。

@Test
public void givenPersonEntitytoPersonWithExpression_whenMaps_thenCorrect()
    Person entity  = new Person();
    entity.setName("Micheal");
    PersonDTO personDto = PersonMapper.INSTANCE.personToPersonDTO(entity);
    assertNull(entity.getId());
    assertNotNull(personDto.getId());
    assertEquals(personDto.getName(), entity.getName());
}

13. 結論

この記事では、MapStructの概要を説明しました。 マッピングライブラリの基本のほとんどと、アプリケーションでの使用方法を紹介しました。

これらの例とテストの実装は、Githubプロジェクトにあります。 これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。