はじめてのSpring Boot で ModelMapper を利用してみる

今年からSpring Bootを始めました!はじめてのSpring Boot を一通り写経して、最近は社内のシステムをRailsからSpring Bootに置き換えて比較などしています。

今回は はじめてのSpring Boot で ModelMapper を利用する方法を試してみたのでまとめてみようと思います。(まちがっているなど突っ込みはWelcom!といか、教えてくださいm(__)m)

ModelMapperとは

ModelMapperとはBean間でマッピングを行うためのマッパーライブラリです。

ModelMapper - Simple, Intelligent, Object Mapping.

はじめてのSpring Boot ではBeagnUtilsを利用していますが、マッピングを実行する制約が多いためModelMapperを利用してみることにしました。

(Railsだとnewするときにattributesで取得したHashを設定することでインスタンスに値を設定しますが、同様のことを行うときに利用します。)

なぜModelMapper?Dozerは利用しない?

はじめてのSpring Boot には Dozer - Dozer も記載があり比較してみましたが、DozerはXMLの設定が中心となりお手軽ではないということで ModelMapper にしました。

機能面での評価などは全く行っておらずとりあえず取っつきやすいものを選択しました。

設定方法

設定はほかのBeanと同様で、以下の設定を行います。

  1. pom.xmlに依存関係を追加する。
  2. AppConfig に Bean を設定する。
  3. 利用するクラスでオートワイヤリングの設定を行う。

具体的には以下の通りとなります。

1.pom.xmlに依存関係を追加する

pom.xmlの dependencies に以下の内容を追加します。

<!--ModelMapperの設定を行う --> 
<dependency>
 <groupId>org.modelmapper.extensions</groupId>
 <artifactId>modelmapper-spring</artifactId>
 <version>0.7.3</version> 
</dependency>

上記の内容は ModelMapper のサイトで確認できます。

ModelMapper - Spring Integration

2.AppConfig に Bean を設定する。

次に利用するBeanを設定します。

package com.example;

...
import org.modelmapper.ModelMapper;
...

@Configuration
public class AppConfig {
    ...

    @Bean
    ModelMapper modelMapper(){
        ModelMapper modelMapper = new ModelMapper();
        return modelMapper;
    }
}

はじめてのSpring Boot の第2章を参考に設定を行いました。

3.利用するクラスでオートワイヤリングの設定を行う。

設定したModelMapperを利用するクラスでオートワイヤリングの設定を行い、ModelMapperオブジェクトのインスタンスをインジェクションします。

package com.example.web;

...
import org.modelmapper.ModelMapper;
...

import java.util.List;

@Controller
@RequestMapping(&quot;customers&quot;)
public class CustomerController {
    ...
    @Autowired
    ModelMapper modelMapper;

    ...
}

@Autowired アノテーションをつけてModelMapperを宣言するだけです。

利用方法

利用方法は簡単で、BeanUtilsで実施している内容をModelMapperで実装します。

package com.example.web;

...
import org.modelmapper.ModelMapper;
...

import java.util.List;

@Controller
@RequestMapping(&quot;customers&quot;)
public class CustomerController {
    ...
    @Autowired
    ModelMapper modelMapper;

    ...

    @RequestMapping(value = &quot;create&quot;, method = RequestMethod.POST)
    String create(@Validated CustomerForm form, BindingResult result, Model model) {
        if (result.hasErrors()) {
            return list(model);
        }

        // note:ModelMapper利用前
//        Customer customer = new Customer();
//        BeanUtils.copyProperties(form, customer);

        // note:ModelMapper利用後
        Customer customer = modelMapper.map(form, Customer.class);

        customerService.create(customer);
        return &quot;redirect:/customers&quot;;
    }

    @RequestMapping(value = &quot;edit&quot;, params = &quot;form&quot;, method = RequestMethod.GET)
    String editForm(@RequestParam Integer id, CustomerForm form) {
        Customer customer = customerService.findOne(id);

        // note:ModelMapper利用前
//        BeanUtils.copyProperties(customer, form);

        // note:ModelMapper利用後
        modelMapper.map(customer, form);

        return &quot;customers/edit&quot;;
    }

    @RequestMapping(value = &quot;edit&quot;, method = RequestMethod.POST)
    String edit(@RequestParam Integer id, @Validated CustomerForm form, BindingResult result) {
        if (result.hasErrors()) {
            return editForm(id, form);
        }

        // note:ModelMapper利用前
//        Customer customer = new Customer();
//        BeanUtils.copyProperties(form, customer);

        // note:ModelMapper利用後
        Customer customer = modelMapper.map(form, Customer.class);

        customer.setId(id);
        customerService.update(customer);
        return &quot;redirect:/customers&quot;;
    }

    ...
}

動作確認

実際に動作してみて正しく値が設定されているかを確認します。確認するためにeditForm(編集ボタンをクリックしたときの処理)にログを仕込みます。

@RequestMapping(value = &quot;edit&quot;, params = &quot;form&quot;, method = RequestMethod.GET)
String editForm(@RequestParam Integer id, CustomerForm form) {
    Customer customer = customerService.findOne(id);

    logger.info(&quot;before copy - form[firstName:&quot; + form.getFirstName() + &quot; lastName:&quot; + form.getLastName() + &quot;]&quot;);

    // note:ModelMapper利用前
//        BeanUtils.copyProperties(customer, form);

    // note:ModelMapper利用後
    modelMapper.map(customer, form);

    logger.info(&quot;after copy - form[firstName:&quot; + form.getFirstName() + &quot; lastName:&quot; + form.getLastName() + &quot;]&quot;);

    return &quot;customers/edit&quot;;
}

実行してみると以下の通り、formの各プロパティに反映されていることがわかります。

com.example.web.CustomerController       : before copy - form[firstName:null lastName:null]
com.example.web.CustomerController       : after copy - form[firstName:Nobita lastName:Nobi]

scopeについて

DIコンテナが管理するインスタンスは シングルトンとして管理されることになります。

ModelMapperはDIを利用しないで各メソッド内で都度インスタンスを生成することも可能ですが、マッピングが変わらない限りは同一のインスタンスを再利用する方がよいとされているため、シングルトンで問題ないようです。

Should I reuse my ModelMapper instance?

Unless you need different mappings between the same types, then it’s best to re-use the same ModelMapper instance. If you use a dependency injection container, you can accomplish this by configuring ModelMapper as a singleton.
ModelMapper - Frequently Asked Questions

なお、DIコンテナのscopeについては著者の 槙さん(@making)から以下のリンクをTwitter経由で教えていただきました。

5. The IoC container

これを読んでみたのですが、BeanにはScopeアノテーションでBeanのスコープ(ライフサイクルのようなもの?)を定義でき、その初期値がSingletonになっていることがわかります。そもそも、DIについて便利ということを意識していましたが、利用する対象によっては利用するかどうかも検討する必要があるということになりそうな気がします。

ちなみに、定義できるスコープは以下のページで確認できます。

Scopes a single bean definition to any number of object instances.

もう少ししたらこの辺も突っ込んでみたいと思います。

はじめてのSpring Boot

はじめてのSpring Bootはとても薄いけど内容が濃くて、ほかの言語やほかのフレームワークをある程度理解しているのであればほとんど躓くことなく読めると思います。私もRailsと比較しながら理解しているところですが、引き続きSpringを利用していきたいと思います。

ソースコードについて

はじめてのSpring Boot の 第3章 「Spring Boot」による「Web アプリ開発」 に該当するサンプルソースコードを利用させていただきました。

書籍のサンプルソースコード

making/hajiboot-samples · GitHub

利用したサンプルソースコード

hajiboot-samples/chapter03/3.3.5.2_hajiboot-thymeleaf at master · making/hajiboot-samples · GitHub

ModelMapperを利用したソースコード

ymanabe / hajiboot-modelmapper — Bitbucket