ModelMapperで1対1に対応しないフィールドのマッピング

最近はSpring Bootで開発する際にModelMapperを使っているのですが、1対1に対応しないようなフィールドをマッピングしたいと思うことがあります。
その方法について試してみたところ、ちょっと苦戦してしまったので今後のためにメモしておきます。

ModelMapperとは

Spring Bootで開発するときにはPOJOでデータを表現し、以下のような場面で使うことになります。

  • コントローラでは、バリデーション用にフォームのクラス
  • Spring Data JPAを使うため、エンティティのクラス
  • Rest APIのJSONと対応するクラス(Mapで代用も可)

これらのクラスで共通して使えるPOJOを作るというのは困難で、似たようなフィールドを持つクラスを作ることになります。
その時にデータを移し替える必要がでてきますが、こういった処理は本質的なロジックではないのに、時間や手間がかかります。
この処理を簡単に実行できるライブラリが、ModelMapperです。

ModelMapperの基本的な使い方については、以下のサイトが詳しいです。
Javaで便利な Beanマッピング ModelMapper

ちなみにSpring向けのIntegrationも用意されているようなので、クラス間でModelMapperを使いまわしたい場合にはこちらを使ったほうがよさそうですね。

なお、ModelMapperはSpring Boot(Spring IO Platform)のDependencyには含まれていないので、バージョン管理は自分で行う必要があります。

1対1に対応しないフィールドのマッピング例

今回は以下のデータを相互に変換します。
なお、fullnameは半角スペースでfirstnameとlastnameが結合されたデータとします。

@Data
class Single {
	private String fullname;
	private String common;
}

@Data
class Multi {
	private String firstname;
	private String lastname;
	private String common;
}

SingleをMultiに変換する場合、fullnameをfirstnameとlastnameに分けてデータを格納します。
これは以下のようにmappingとconverterを設定をすることで実現できます。

ModelMapper mapper = new ModelMapper();
mapper.addMappings(new PropertyMap<Single, Multi>() {
    @Override
    protected void configure() {
        using(new Converter<Single, String>(){
            public String convert(MappingContext<Single, String> context) {
                return context.getSource().getFullname().split(" ")[0];
            }
        }).map(source, destination.getFirstname());

        using(new Converter<Single, String>(){
            public String convert(MappingContext<Single, String> context) {
                return context.getSource().getFullname().split(" ")[1];
            }
        }).map(source, destination.getLastname());
    }
});

MultiをSingleに変換する場合、firstnameとlastnameを結合し、fullnameとして格納します。
これは以下のようにmappingとconverterを設定することで実現できます。

ModelMapper mapper = new ModelMapper();
mapper.addMappings(new PropertyMap<Multi, Single>() {
    @Override
    protected void configure() {
        using(new Converter<Multi, String>(){
            public String convert(MappingContext<Multi, String> context) {
                return context.getSource().getFirstname() + " "
                + context.getSource().getLastname();
            }
        }).map(source, destination.getFullname());
    }
});

サンプルのソースコード全体はGithubにあります。
この出力結果は以下のようになります。

Multi(firstname=First, lastname=Last, common=common)
Single(fullname=First Last, common=common)

まとめ

最初はドキュメントを読んでも分からず、試行錯誤や情報収集の末になんとか実装できました。
ModelMapperはフィールドが1対1に対応していなくてもマッピングや変換処理を定義できる柔軟性があることが分かったので、これからも積極的に使っていきたいと思います。

参考

https://stackoverflow.com/questions/47297689/how-to-map-multiple-fields-into-one-destination-field-using-modelmapper

関連記事


  1. Spring Web MVCのAuto Configuration周辺のクラス図を描いてみた
  2. Javaでオブジェクト配列からフィールド配列を生成
  3. Spring Boot アノテーション集
  4. Javaサーブレットにおける部分URLやファイルパスの取得
  5. JJUG CCC 2019 Springに参加してきました
  6. Spring BootアプリにFlywayを導入してみた
  7. MavenでOSSのライセンス一覧を出力する

comments powered by Disqus