MapStructのクイックガイド

1概要

この記事では、簡単に言うと、http://www.mapstruct.org/[MapStruct]の使用法を探ります。

このAPIには、2つのJava Beans間で自動的にマッピングされる機能が含まれています。 MapStructを使えば、インターフェースを作成するだけでよく、ライブラリはコンパイル時に自動的に具象実装を作成します。

2 MapStructとTransfer Objectのパターン

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

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

そのため、MapStructが - 手動でBeanマッパーを作成するのは時間がかかります。ライブラリはBeanマッパークラスを自動的に生成することができます。

3 Maven

Mavenの__pom.xmlに以下の依存関係を追加しましょう。

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-jdk8</artifactId>
    <version>1.1.0.Final</version>
</dependency>

Mapstruct および彼の最新の安定版リリースhttps://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.mapstruct%22%20AND%20a%3A%22mapstruct-processor%22[processor]は両方ともMaven Centralから入手可能です。リポジトリ

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

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

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <configuration>
        <source>1.8</source>
    <target>1.8</target>
        <annotationProcessorPaths>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.1.0.Final</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

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クリーンインストール を実行することで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 SimpleSourceDestinationMapperTest {
    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でマッパーのインスタンスを取得しましょう。

もちろん、これはインスタンスを取得するための非常に手動の方法です。それよりはるかに良い方法は、必要な場所に直接マッパーをインジェクトすることです(私たちのプロジェクトがDependency Injectionソリューションを使用する場合)。

  • 幸いにもMapStructはSpringとCDIの両方をしっかりサポートしています** ( コンテキストと依存性注入

MapperでSpring IoCを使用するには、 @ Mapper に値 spring を付けて __componentModel attributeを追加する必要があります。CDIの場合は cdi__になります。

5.1. マッパーを変更する

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

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

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

前の例から、MapStructはBeanが同じフィールド名を持っているので、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 アノテーションを追加する必要があります。このアノテーションは、target属性とsource属性を追加するために使用する @ 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. テストケース

ここでも、ソースとデスティネーションの両方のオブジェクト値が一致することをテストする必要があります。

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

その他のテストケースはhttps://github.com/eugenp/tutorials/tree/master/mapstruct[Githubプロジェクト]にあります。

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. マッパーを変更する

ここでは、 Division DivisionDTO に、またはその逆に変換するメソッドを追加する必要があります。 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には、いくつかの既成の暗黙の型変換もあります。この例では、Stringの日付を実際の Date オブジェクトに変換します。

暗黙的な型変換の詳細については、http://mapstruct.org/documentation/1.0/reference/html/#implicit-type-conversionversion[MapStruct reference guide]を参照してください。

8.1. 豆を変更する

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

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能力を超えるようにマッパーをカスタマイズしたいかもしれません。

たとえば、型変換に加えて、以下の例のように何らかの方法で値を変換することがあります。

このような場合、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
}

ここで注意が必要なのは、 BigDecimal total の金額を 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<TransactionDTO> toTransactionDTO(Collection<Transaction> transactions);
}

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

一方、 __Collection List abstractにマッピングするためのメソッドは残したので、 MapStruct __でそれを実装します。

9.3. 生成結果

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

@Generated
class TransactionMapperImpl extends TransactionMapper {

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

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

        return list;
    }
}

12行目でわかるように、 __MapStruct __は、生成されたメソッド内の実装を使用します。

10結論

この記事ではMapStructの概要を説明しました。MapSライブラリの基本の大部分とそれをアプリケーションで使用する方法を紹介しました。

これらの例とテストの実装はhttps://github.com/eugenp/tutorials/tree/master/mapstruct[Github]プロジェクトにあります。これはMavenプロジェクトなので、そのままインポートして実行するのは簡単なはずです。