Java開発のお供に ~ MapStruct編

# はじめに

こんにちは。技術を学ぶことが好きな T です。

現場に初めて参画される方や API 開発をこれから行っていく方を助けたいという思いから、記事を作成させていただきました。
タイトルにあるように【MapStruct】というライブラリに関して紹介、及び実装例に関して詳細させていただきます。
微力でも皆様のお役に立てれば幸いです。
※ 最適解というわけでなく、一実装方法として確認いただけると幸いです。

### 対象者

- 現場に初めて参画される方
- 駆け出しエンジニア(WEB アプリの基礎がある程度わかっている)
- API 開発を実施する方

# 導入

- Javaを利用した開発では、Bean[^1]という考え方が出てきます。
- Bean には、さまざまなものがあり、Form や Dto、Entity 等ざまざまです。
  レイヤードアーキテクチャ[^2]を採用しているプロジェクト等では、各層にて利用できる Bean が決まっていて、各層の間で Bean の詰め替え作業が発生します。
- Getter, Setter, Builder[^3] を利用して詰め替えを行うことができるのですが、Bean 内部の値が変動した場合に変更箇所が多くなり、可用性にかけてしまいます。
- そこで登場するのが Bean Mapping をサポートしてくれるライブラリーです。今回は、「Mapstruct」に関して纏めていきます。

# MapStruct 概要

- Java の BeanMapping ライブラリ。
- ドキュメントが充実していて、利用しやすい。
- 他の BeanMapping ライブラリ[^4]に比べて処理速度が早い。
- コンパイル時に、Mapping を行う実装クラスを自動生成してくれる。

# MapStruct 設定方法

下記では、Token.javaをTokenResponse.javaに詰め替える処理を表現しています。   
@Mapperというアノテーションを該当のInterfaceに付与することで完了です。   
実装クラスに関しては、保存したタイミングで下記に自動生成されます。
{rootName}/build/generated/sources/annotationProcessor/java/main/    

※ 上記パスはデフォルトパスです。設定によって変更できると考えています。

- 想定される環境(当時確認した環境)

  - Java (version 17)
  - Gradle (version 7.5)
  - Spring Boot (version 2.7.5)
  - Editor: IntelliJ

- Gradle 設定
  - ① 等のコメントのある下部に記載されている行が必要です。
  - あくまで個人開発レベルでの設定です。最適解はプロジェクトごとにあると考えてください。

```gradle
plugins {
    id 'org.springframework.boot' version '2.7.3'
    id 'io.spring.dependency-management' version '1.0.13.RELEASE'
    id 'java'
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok:1.18.24'
    annotationProcessor 'org.projectlombok:lombok:1.18.24'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    ①MapStructの記述を追加します。
    // https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor
    implementation group: 'org.mapstruct', name: 'mapstruct', version: '1.5.2.Final'
    // https://mvnrepository.com/artifact/org.mapstruct/mapstruct
    implementation group: 'org.mapstruct', name: 'mapstruct-processor', version: '1.5.2.Final'
    annotationProcessor group: 'org.mapstruct', name: 'mapstruct-processor', version: '1.5.2.Final'
~~~~
}
```

- Mapper クラス
  - Mapper クラス(Interface で定義すること)
  - @Mapper を付与することで、Bean の Mapping が可能となります。

```java
@Mapper
public interface TokenMapper {
    TokenResponse toTokenResponse(Token domain);

    Token toDomain(String token);
}
```

- TokenResponse.java
  - @Data は Lombok を利用しています。

```java
@Data
public class TokenResponse {

  private String token = null;

}
```

- Token.java
    - 「@」で始まっているものはすべてLombokの機能です。
```java
@Data
@Builder(access = AccessLevel.PRIVATE)
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
public class Token  {
    private String token;
}
```


# MapStruct 私的おすすめ設定

MapStructはカスタム要素が豊富です。    
よく扱うのは基盤となるMapper設定を作成し、各Mapperに反映させる手法や独自のMappingを定義する方法です。

-  基盤Mapper
    - componentModel => DIを適応させる際に付与する記述です。(springframeworkで利用する場合はspringでいいでしょう。)
    - unmappedTargetPolicy => マッピングのポリシーを設定できます。   
    ReportingPolicy.IGNOREと設定した場合、マッピングされていないプロパティに関しては無視されます。     
    (※マッピング間に異なるプロパティを持つ場合、同じプロパティのいみマッピングを行う。異なるプロパティは、無視をする。)     
    他にもERRORやWARNが存在しています。
    - nullValueMappingStrategy => null値の制御を行うことができます。     
    NullValueMappingStrategy.RETURN_NULLトセってした場合、マッピング元がNullである場合に、マッピング先のプロパティにNullが設定されます。
    - 使用方法は、該当のMapperに記述を追加するだけです。()
```java
@MapperConfig(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL)
public interface ConfigMapper {
}
```
```java
@Mapper(config = ConfigMapper.class)
public interface TokenMapper 
```

- カスタムマッピング
    - カスタムマッピングのやり方は様々あります。
    - 今回はあまり見かけないやり方でマッピング元とマッピング先で型違う場合や、特定の処理を行う手法です。
    - 下記例では、StringをAccountNameというクラスにマッピングしようとしています。    
        valueToAccountNameConverterメソッドに@Namedと記載が見られます。    
        @Namedにつけられた名前を参照してマッピングを行うことができます。
        
    - MemberMapperで使用する場合に@Mapper(uses)を定義します。     
    定義することで該当のクラスで定義されているメソッドを呼び出すことができます。     
```java
@Component
@Named("EnumConverter")
public class EnumConverter {

    @Named("toAccountName")
    public AccountName valueToAccountNameConverter(String target) {
        return AccountName.build(target);
    }
```
```java
@Mapper(uses = EnumConverter.class, config = ConfigMapper.class)
public interface MemberMapper {
    // target = マッピング先, qualifiedByName = {マッピング定義クラス : マッピングメソッドの@Named名}
    @Mapping(target = "accountName", qualifiedByName = {"EnumConverter", "toAccountName"})
    Member toDomain(MemberEntity member);
```

# 終わりに

MapStruct に関して紹介させていただきました。  
参考になれば幸いです。
BeanMappingを行うライブラリに関してはBeanUtils.copyProperties等のデフォルトでSpringに組み込まれているものもあります。各プロジェクトで適切なものを選定するのがいいのかなと思います。
※ ModelMapperも利用したことがあるのですが、解決不明な問題に陥り、MapStructに逃げ込んできました。

ドキュメントがとても充実しておりますので、もし現場で使うことになったら目を通しておくといいですね。
https://mapstruct.org/documentation/stable/reference/html/#mapping-result-for-null-arguments

※ ソースすべてや用語一つ一つを纏めることができず、申し訳ございません。  
※ 毎度のことで申し訳ございませんが下記プロジェクトで採用しています。興味があればぜひ。   
https://github.com/konkon-T/experimentally-java

[^1]: データを運搬するための入れ物のようなイメージです。
[^2]: レイヤードアーキテクチャに関してわかりやすく纏められている方のサイトです。https://hafilog.com/layered-architecture
[^3]: Lombokというライブラリーを使用すること@を定義して簡単にGetterやSetter、Builderを実装することができます。
[^4]: 各ライブラリーの比較を行っているサイトです。https://www.baeldung.com/java-performance-mapping-frameworks