ModelMapperの使い方

はじめに

こんにちは、新卒2年目の岡崎です。今回は、ModelMapperの使い方について紹介します。

環境

  • Spring boot
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.2)
openjdk version "21.0.2" 2024-01-16 LTS
OpenJDK Runtime Environment Corretto-21.0.2.13.1 (build 21.0.2+13-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.2.13.1 (build 21.0.2+13-LTS, mixed mode, sharing)

ModelMapperについて

知っている人も多いと思いますが、改めておさらいしていきたいと思います。ModelMapperでは、違う型同士の値をコピーし、反映することができます。

例えばAオブジェクトとBオブジェクトがあり、AオブジェクトからBオブジェクトへ値を反映させたいケースがあったとします。この時、ModelMapperを使って行うこともでき、とても便利です。

それでは、実際にコードで見てみましょう!

準備

build.gradleに以下の実装をしてください。

dependencies {
  // model mapper
  implementation 'org.modelmapper:modelmapper:{モデルマッパーの最新バージョン}'

  // lombok
  compileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
}

実装

以下のように、NovelDtoオブジェクトとNovelオブジェクトが存在していると仮定します。

@Data
@Accessors(chain = true)
public class NovelDto {
    /**
     * ID
     */
    private Long id;

    /**
     * ユーザーID
     */
    private Long userId;

    /**
     * 高評価数
     */
    private Long likeTotal;

    /**
     * 低評価数
     */
    private Long dislikeTotal;

    /**
     * 高評価したかどうか
     */
    private Boolean isLiked = false;

    /**
     * 低評価したかどうか
     */
    private Boolean isDisliked = false;

    /**
     * 自分の小説かどうか
     */
    private Boolean isMyNovel = false;

    /**
     * 作成日時
     */
    private LocalDateTime createdAt;
}
@Data
@Accessors(chain = true)
public class Novel {
    /**
     * ID
     */
    private Long id;

    /**
     * ユーザーID
     */
    private Long userId;

    /**
     * 高評価数
     */
    private Long likeTotal;

    /**
     * 低評価数
     */
    private Long dislikeTotal;

    /**
     * 高評価したかどうか
     */
    private Boolean isLiked = false;

    /**
     * 低評価したかどうか
     */
    private Boolean isDisliked = false;

    /**
     * 自分の小説かどうか
     */
    private Boolean isMyNovel = false;

    /**
     * 作成日時
     */
    private LocalDateTime createdAt;

    /**
     * オブジェクトが空かどうか判定する.
     *
     * @return オブジェクトが空かどうか
     */
    public Boolean isEmpty() {
        return Objects.isNull(this.commentId);
    }
}

ほとんどのプロジェクトでは層によって使用するオブジェクトが決まっていると思います。例えば、Service層ではRepository層から受け取ったNovelDtoオブジェクトを、Novelオブジェクトへ反映します。

この時の実装例を以下に示します。

@Service
@RequiredArgsConstructor
public class NovelService {
    private final NovelRepository novelRepository;

    public Novel getNovel(Long id) {
        final NovelDto dto = novelRepository.findNovel(id);

        // 中略

        final Novel novel = new Novel()
            .setid(dto.getId())
            .setUserId(dto.getUserId())
            .setLikeTotal(dto.getLikeTotal())
            .setDislikeTotal(dto.getDislikeTotal())
            .setIsLiked(dto.getIsLiked())
            .setIsDisliked(dto.getIsDisliked())
            .setIsMyNovel(dto.getIsMyNovel());

        return novel;
    }
}

このコードの問題点は、setし忘れが起きてバグの原因になる可能性があることです。(もしかすると、このようなコードが煩雑に感じる人もいるかもしれません。)

ModelMapperを使えば、以下のようにコードを書けます。

@Service
@RequiredArgsConstructor
public class NovelService {
    private final NovelRepository novelRepository;
    private final ModelMapper modelMapper;

    public Novel getNovel(Long id) {
        final NovelDto dto = novelRepository.findNovel(id);
        
        return modelMapper.map(dto, Novel.class);
    }
}

一行で書くことができるので、コードはシンプルになり、setのし忘れは起きないようになります。

ちなみにListのオブジェクトの反映は、以下のように行います。

List.of(modelMapper.map(dtoList, 反映したいclass[].class));

補足

ModelMapperを使って開発していると、UnrecognizedPropertyExceptionが発生することがあります。

これはオブジェクト同士を比較した時、存在しない属性があることが原因です。

様々な解決方法があると思いますが、今回は@JsonIgnoreを使う方法を紹介します。

まずは、build.gradleに依存関係を追加します。

dependencies {
        implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:${springMybatisStarterVersion}"
}

そして、オブジェクト同士を比較して存在しない属性に対し、@JsonIgnoreを追加します。

@Data
@Accessors(chain = true)
public class Novel {
    /**
     * ID
     */
    private Long id;

   // 中略

    /**
     * オブジェクトが空かどうか判定する.
     *
     * @return オブジェクトが空かどうか
     */
    @JsonIgnore
    public Boolean isEmpty() {
        return Objects.isNull(this.commentId);
    }
}

これでエラーが発生しなくなりました。

最後に

今回は、ModelMapperの使い方を紹介しました。ModelMapperはとても便利なので、使ってみてください。

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があればぜひぜひ連絡ください!

www.wantedly.com