Beanマッピング(Dozer) -------------------------------------------------------------------------------- .. only:: html .. contents:: 目次 :depth: 4 :local: | Overview ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Beanマッピングは、一つのBeanを他のBeanにフィールド値をコピーすることである。 | アプリケーションの異なるレイヤ間(アプリケーション層とドメイン層)で、データの受け渡しをする場合など、Beanマッピングが必要となるケースは多い。 | 例として、アプリケーション層の\ ``AccountForm``\ オブジェクトを、ドメイン層の\ ``Account``\ オブジェクトに変換する場合を考える。 | ドメイン層は、アプリケーション層に依存してはならないため、AccountFormオブジェクトをそのままドメイン層で使用できない。 | そこで、\ ``AccountForm``\ オブジェクトを、\ ``Account``\ オブジェクトにBeanマッピングし、ドメイン層では、\ ``Account``\ オブジェクトを使用する。 | これによって、アプリケーション層と、ドメイン層の依存関係を一方向に保つことができる。 .. figure:: ./images/beanmapping-overview.png :width: 80% | このオブジェクト間のマッピングは、Beanのgetter/setterを呼び出して、データの受け渡しを行うことで実現できる。 | しかしながら、処理が煩雑になり、プログラムの見通しが悪くなるため、本ガイドラインでは、BeanマッピングライブラリであるOSSで利用可能な `Dozer `_ を使用することを推奨する。 | Dozerを使用することで下図のように、コピー元クラスとコピー先クラスで型が異なるコピーや、ネストしたBean同士のコピーも容易に行うことができる。 .. figure:: ./images/dozer-functionality-overview.png :width: 75% Dozerをした場合と使用しない場合のコード例を挙げる。 * 煩雑になり、プログラムの見通しが悪くなる例 .. code-block:: java User user = userService.findById(userId); XxxOutput output = new XxxOutput(); output.setUserId(user.getUserId()); output.setFirstName(user.getFirstName()); output.setLastName(user.getLastName()); output.setTitle(user.getTitle()); output.setBirthDay(user.getBirthDay()); output.setGender(user.getGender()); output.setStatus(user.getStatus()); * Dozerを使用した場合の例 .. code-block:: java User user = userService.findById(userId); XxxOutput output = beanMapper.map(user, XxxOutput.class); 以降は、Dozerの利用方法について説明する。 .. note:: Dozer 6.4.0より、JSR-310 Date and Time APIが提供する以下のクラスのマッピングがサポートされた。 対象クラス : * \ ``java.time.LocalDate``\ * \ ``java.time.LocalTime``\ * \ ``java.time.LocalDateTime``\ * \ ``java.time.OffsetTime``\ * \ ``java.time.OffsetDateTime``\ * \ ``java.time.ZonedDateTime``\ .. note:: **Java SE 11環境にてDozerを利用する場合** Dozer 6.3.0より、マッピング定義XMLファイルの解析にデフォルトでJAXBが利用されるようになった。 Dozer 6.5.0より、Mavenを利用してJava SE 9以降でビルドするとjaxb-runtimeへの依存が推移的に解決されるため、JAXBを利用するために特別な設定を施す必要はない。 | How to use ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Dozerは、Java Beanのマッピング機能ライブラリである。 変換元のBeanから変換先のBeanに、再帰的(ネストした構造)に、値をコピーする。 .. _bean-mapper-definition: Dozerを使用するためのBean定義 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" Dozerは、単独で使用するとき、以下のように、\ ``com.github.dozermapper.core.DozerBeanMapperBuilder``\ を利用してMapper のインスタンスを作成する。 .. code-block:: java Mapper mapper = DozerBeanMapperBuilder.buildDefault(); Mapper のインスタンスを毎回作成するのは、効率が悪いため、 Dozerが提供している\ ``com.github.dozermapper.spring.DozerBeanMapperFactoryBean``\ を使用すること。 Bean定義ファイル(applicationContext.xml)に、Mapperを作成するFactoryクラスである\ ``com.github.dozermapper.spring.DozerBeanMapperFactoryBean``\ を定義する .. code-block:: xml .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | mappingFilesに、マッピング定義XMLファイルを指定する。 | \ ``com.github.dozermapper.spring.DozerBeanMapperFactoryBean``\ は、 interfaceとして \ ``com.github.dozermapper.core.Mapper``\ を保持している。そのため、 \ ``@Inject``\ 時は \ ``Mapper``\ を指定する。 | この例では、クラスパス直下の、/META-INF/dozerの任意フォルダ内の | (任意の値)-mapping.xmlを、すべて読み込む。このXMLファイルの内容については、以降で説明する。 | Beanマッピングを行いたいクラスに、\ ``Mapper``\ をインジェクトすればよい。 .. code-block:: java @Inject Mapper beanMapper; .. _beanconverter-basic-mapping-label: Bean間のフィールド名、型が同じ場合のマッピング """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" デフォルトの動作として、Dozerは対象のBean間のフィールド名が同じであれば、マッピング定義XMLファイルを作成せずにマッピングできる。 変換元のBean定義 .. code-block:: java public class Source { private int id; private String name; // omitted setter/getter } 変換先のBean定義 .. code-block:: java public class Destination { private int id; private String name; // omitted setter/getter } 以下のように、\ ``Mapper``\ の \ ``map``\ メソッドを使ってBeanマッピングを行う。 下記メソッドを実行した後、Destinationオブジェクトが新たに作成され、sourceの各フィールドの値が作成されたDestinationオブジェクトにコピーされる。 .. code-block:: java Source source = new Source(); source.setId(1); source.setName("SourceName"); Destination destination = beanMapper.map(source, Destination.class); // (1) System.out.println(destination.getId()); System.out.println(destination.getName()); .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | 第一引数に、コピー元のオブジェクトを渡し、第二引数に、コピー先のBeanのクラスを渡す。 上記のコードを実行すると以下のように出力される。作成されたオブジェクトにコピー元のオブジェクトの値が設定されていることが分かる。 .. code-block:: console 1 SourceName 既に存在している\ ``destination``\ オブジェクトに、\ ``source``\ オブジェクトのフィールドをコピーしたい場合は、 .. code-block:: java Source source = new Source(); source.setId(1); source.setName("SourceName"); Destination destination = new Destination(); destination.setId(2); destination.setName("DestinationName"); beanMapper.map(source, destination); // (1) System.out.println(destination.getId()); System.out.println(destination.getName()); .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | 第一引数に、コピー元のオブジェクトを渡し、第二引数に、コピー先のオブジェクトを渡す。 上記のコードを実行すると以下のように出力される。コピー元のオブジェクトの値がコピー先に反映されていることが分かる。 .. code-block:: console 1 SourceName .. note:: \ ``Destination``\ クラスのフィールドで\ ``Source``\ クラスに存在しないものは、コピー前後で値は変わらない。 変換元のBean定義 .. code-block:: java public class Source { private int id; private String name; // omitted setter/getter } 変換先のBean定義 .. code-block:: java public class Destination { private int id; private String name; private String title; // omitted setter/getter } マッピング例 .. code-block:: java Source source = new Source(); source.setId(1); source.setName("SourceName"); Destination destination = new Destination(); destination.setId(2); destination.setName("DestinationName"); destination.setTitle("DestinationTitle"); beanMapper.map(source, destination); System.out.println(destination.getId()); System.out.println(destination.getName()); System.out.println(destination.getTitle()); 上記のコードを実行すると以下のように出力される。\ ``Source``\ クラスには\ ``title``\ フィールドが ないため、\ ``Destination``\ オブジェクトの\ ``title``\ フィールドは、コピー前のフィールド値から変更がない。 .. code-block:: console 1 SourceName DestinationTitle .. _beanconverter-difference-type-mapping-label: Bean間のフィールド名は同じ、型が異なる場合のマッピング """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" コピー元と、コピー先でBeanのフィールドの型が異なる場合、 型変換がサポートされている型は、自動でマッピングできる。 以下のような変換は、マッピング定義XMLファイル無しで変換できる。 例 : String -> BigDecimal 変換元のBean定義 .. code-block:: java public class Source { private String amount; // omitted setter/getter } 変換先のBean定義 .. code-block:: java public class Destination { private BigDecimal amount; // omitted setter/getter } マッピング例 .. code-block:: java Source source = new Source(); source.setAmount("123.45"); Destination destination = beanMapper.map(source, Destination.class); System.out.println(destination.getAmount()); 上記のコードを実行すると以下のように出力される。型が異なる場合でも値をコピーできていることが分かる。 .. code-block:: console 123.45 サポートされている型変換については、 `マニュアル `_ を参照されたい。 .. _beanconverter-difference-item-xml-mapping-label: Bean間のフィールド名が異なる場合のマッピング """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" コピー元と、コピー先でフィールド名が異なる場合、マッピング定義XMLファイルを作成し、 Beanマッピングするフィールドを定義することで変換できる。 変換元のBean定義 .. code-block:: java public class Source { private int id; private String name; // omitted setter/getter } 変換先のBean定義 .. code-block:: java public class Destination { private int destinationId; private String destinationName; // omitted setter/getter } \ :ref:`bean-mapper-definition`\ の定義がある場合、 src/main/resources/META-INF/dozerフォルダ内に、(任意の値)-mapping.xmlという、マッピング定義XMLファイルを作成する。 .. code-block:: xml com.xx.xx.Source com.xx.xx.Destination id destinationId name destinationName .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ````\ タグ内にコピー元のBeanの、完全修飾クラス名(FQCN)を指定する。 * - | (2) - | \ ````\ タグ内にコピー先のBeanの、完全修飾クラス名(FQCN)を指定する。 * - | (3) - | \ ````\ タグ内の\ ````\ タグ内にコピー元のBeanの、マッピング用のフィールド名を指定する。 * - | (4) - | \ ````\ タグ内の\ ````\ タグ内に(3)に対応するコピー先のBeanの、マッピング用のフィールド名を指定する。 マッピング例 .. code-block:: java Source source = new Source(); source.setId(1); source.setName("SourceName"); Destination destination = beanMapper.map(source, Destination.class); // (1) System.out.println(destination.getDestinationId()); System.out.println(destination.getDestinationName()); .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | 第一引数に、コピー元のオブジェクトを渡し、第二引数に、コピー先のBeanのクラスを渡す。(基本マッピングと違いはない。) 上記のコードを実行すると以下のように出力される。 .. code-block:: console 1 SourceName \ :ref:`bean-mapper-definition`\ の設定によって、\ ``mappingFiles``\ プロパティにクラスパス直下のMETA-INF/dozer配下に存在するマッピング定義XMLファイルが読み込まれる。 ファイル名は(任意の値)-mapping.xmlである必要がある。 いずれかのファイル内に\ ``Source``\ クラスと\ ``Destination``\ クラス間におけるマッピング定義があれば、その設定が適用される。 .. note:: マッピング定義XMLファイルは、Controller単位で作成し、ファイル名は、(Controller名からControllerを除いた値)-mapping.xmlにすることを推奨する。 例えば、TodoControllerに対するマッピング定義XMLファイルは、src/main/resources/META-INF/dozer/todo-mapping.xmlに作成する。 .. note:: 本ガイドラインでは解説しないが、マッピング定義XMLファイルにおいてEL式を使用することができる。 EL式の解釈にはJakarta EE(Java EE)の標準APIを用いており、デフォルトでは\ ``com.sun.el.ExpressionFactoryImpl``\ クラスが利用される。 利用する実装クラスは\ ``javax.el.ExpressionFactory``\ システムプロパティにより切り替えることが可能である。 なお、ブランクプロジェクトのデフォルト設定では依存関係に標準APIの実装ライブラリが存在しないため 実行環境によっては起動時ログに以下のような警告が表示されるが、EL式を利用しない場合は実行に支障はないため無視して良い。 .. code-block:: console X-Track: level:WARN logger:c.github.dozermapper.core.el.ELExpressionFactory message:javax.el is not supported; Failed to resolve ExpressionFactory, com.sun.el.ExpressionFactoryImpl 詳細は、`Expression Language `_ を参照されたい。 | 単方向・双方向マッピング """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" .. _beanconverter-one-two-way-mapping-label: マッピングXMLで定義されているマッピングは、デフォルトで、双方向マッピングである。 すなわち前述の例では\ ``Source``\ オブジェクトから\ ``Destination``\ オブジェクトへのマッピングを行ったが、 \ ``Destination``\ オブジェクトから\ ``Source``\ オブジェクトのマッピングも可能である。 単方向のみを指定したい場合は、マッピング・フィールド定義に、\ ````\ タグの\ ``type``\ 属性に\ ``one-way``\ を設定する。 .. code-block:: xml com.xx.xx.Source com.xx.xx.Destination id destinationId name destinationName 変換元のBean定義 .. code-block:: java public class Source { private int id; private String name; // omitted setter/getter } 変換先のBean定義 .. code-block:: java public class Destination { private int destinationId; private String destinationName; // omitted setter/getter } マッピング例 .. code-block:: java Source source = new Source(); source.setId(1); source.setName("SourceName"); Destination destination = beanMapper.map(source, Destination.class); System.out.println(destination.getDestinationId()); System.out.println(destination.getDestinationName()); 上記のコードを実行すると以下のように出力される。 .. code-block:: console 1 SourceName 単方向を指定している場合に、逆方向のマッピングを行ってもエラーは発生しない。コピー処理は無視される。 なぜなら、マッピング定義がないと\ ``Destination``\ のフィールドに該当する\ ``Source``\ のフィールドが存在ないとみなされるためである。 .. code-block:: java Destination destination = new Destination(); destination.setDestinationId(2); destination.setDestinationName("DestinationName"); Source source = new Source(); source.setId(1); source.setName("SourceName"); beanMapper.map(destination, source); System.out.println(source.getId()); System.out.println(source.getName()); 上記のコードを実行すると以下のように出力される。 .. code-block:: console 1 SourceName .. note:: **Dozer 6.1.0以前のバージョンに存在する単方向マッピングのバグについて** Dozer 6.1.0以前では、同名フィールドは\ ````\ タグの\ ``type``\ 属性に\ ``one-way``\ を付与しても正常に単方向マッピングとならず、逆方向でもマッピングされるバグが存在する。 TERASOLUNA Server Framework for Java 5.4.X以前はDozer 6.1.0以前のバージョンを使用しているため、バグの影響を受けていた。 具体的には、\ ````\ タグの\ ``type``\ 属性に\ ``one-way``\ を付与した場合、フィールドが別名であれば正常に単方向マッピングとなる。 それ以外の項目は双方向マッピングされてしまう。 具体例を以下に示す。 変換元のBean定義 .. code-block:: java public class Source { private int id; private String sameNameField1; private String sameNameField2; // omitted setter/getter } 変換先のBean定義 .. code-block:: java public class Destination { private int destinationId; private String sameNameField1; private String sameNameField2; // omitted setter/getter } マッピング定義 .. code-block:: xml xxx.Source xxx.Destination id destinationId sameNameField1 sameNameField1 上記のようにマッピング定義した場合、\ ``sameNameField1``\ 、\ ``sameNameField2``\ は逆方向にもマッピングされてしまっていた。 .. _beanconverter-custom-converter-label: Nestしたフィールドのマッピング """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" コピー元Beanが持つフィールドを、コピー先Beanが持つNestしたBeanのフィールドにも、マッピングできることである。 (Dozerの用語で、 `Deep Mapping `_ と呼ばれる。) 変換元のBean定義 .. code-block:: java public class EmployeeForm { private int id; private String name; private String deptId; // omitted setter/getter } 変換先のBean定義 .. code-block:: java public class Employee { private Integer id; private String name; private Department department; // omitted setter/getter } .. code-block:: java public class Department { private String deptId; // omitted setter/getter and other fields } 例 : \ ``EmployeeForm``\ オブジェクトが持つ\ ``deptId``\ を、\ ``Employee``\ オブジェクトが持つ\ ``Department``\ の\ ``deptId``\ にマップしたい場合、 以下のように定義する。 .. code-block:: xml com.xx.aa.EmployeeForm com.xx.bb.Employee deptId department.deptId .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``Employee``\ フォームの\ ``deptId``\ に対する、\ ``Employee``\ オブジェクトのフィールドを指定する。 マッピング例 .. code-block:: java EmployeeForm source = new EmployeeForm(); source.setId(1); source.setName("John"); source.setDeptId("D01"); Employee destination = beanMapper.map(source, Employee.class); System.out.println(destination.getId()); System.out.println(destination.getName()); System.out.println(destination.getDepartment().getDeptId()); 上記のコードを実行すると以下のように出力される。 .. code-block:: console 1 John D01 上記の場合は、変換先クラスである\ ``Employee``\ の新規インスタンスが作成される。 \ ``Employee``\ の中の\ ``department`` フィールドにも、新規に作成された\ ``Department``\ インスタンスが設定され、 \ ``EmployeeForm``\ の\ ``deptId``\ が、コピーされる。 下記のように\ ``Employee``\ の中の\ ``department`` フィールドに既に\ ``Department``\ オブジェクトが設定されている場合は、 新規インスタンスは作成されず、既存の\ ``Department``\ オブジェクトの\ ``deptId``\ フィールドに、 \ ``EmployeeForm``\ の\ ``deptId``\ がコピーされる。 .. code-block:: java EmployeeForm source = new EmployeeForm(); source.setId(1); source.setName("John"); source.setDeptId("D01"); Employee destination = new Employee(); Department department = new Department(); destination.setDepartment(department); beanMapper.map(source, destination); System.out.println(department.getDeptId()); System.out.println(destination.getDepartment() == department); 上記のコードを実行すると以下のように出力される。 .. code-block:: console D01 true | Collectionマッピング """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" Dozerは、以下のCollectionタイプの双方向自動マッピングをサポートしている。 フィールド名が同じである場合、マッピング定義XMLファイルが不要である。 * \ ``java.util.List``\ <=> ``java.util.List``\ * \ ``java.util.List``\ <=> Array * Array <=> Array * \ ``java.util.Set``\ <=> \ ``java.util.Set``\ * \ ``java.util.Set``\ <=> Array * \ ``java.util.Set``\ <=> \ ``java.util.List``\ 次のクラスのコレクションをもつBeanのマッピングについて考える。 .. code-block:: java package com.example.dozer; public class Email { private String email; public Email() { } public Email(String email) { this.email = email; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } @Override public String toString() { return email; } // generated by Eclipse @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((email == null) ? 0 : email.hashCode()); return result; } // generated by Eclipse @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Email other = (Email) obj; if (email == null) { if (other.email != null) return false; } else if (!email.equals(other.email)) return false; return true; } } 変換元のBean .. code-block:: java package com.example.dozer; import java.util.List; public class AccountForm { private List emails; public void setEmails(List emails) { this.emails = emails; } public List getEmails() { return emails; } } 変換先のBean .. code-block:: java package com.example.dozer; import java.util.List; public class Account { private List emails; public void setEmails(List emails) { this.emails = emails; } public List getEmails() { return emails; } } マッピング例 .. code-block:: java AccountForm accountForm = new AccountForm(); List emailsSrc = new ArrayList(); emailsSrc.add(new Email("a@example.com")); emailsSrc.add(new Email("b@example.com")); emailsSrc.add(new Email("c@example.com")); accountForm.setEmails(emailsSrc); Account account = beanMapper.map(accountForm, Account.class); System.out.println(account.getEmails()); 上記のコードを実行すると以下のように出力される。 .. code-block:: console [a@example.com, b@example.com, c@example.com] ここまではこれまで説明したことと特に変わりはない。 次の例のように、\ **コピー先のBeanのCollectionフィールドに既に要素が追加されている場合は要注意である。**\ .. code-block:: java AccountForm accountForm = new AccountForm(); Account account = new Account(); List emailsSrc = new ArrayList(); List emailsDest = new ArrayList(); emailsSrc.add(new Email("a@example.com")); emailsSrc.add(new Email("b@example.com")); emailsSrc.add(new Email("c@example.com")); emailsDest.add(new Email("a@example.com")); emailsDest.add(new Email("d@example.com")); emailsDest.add(new Email("e@example.com")); accountForm.setEmails(emailsSrc); account.setEmails(emailsDest); beanMapper.map(accountForm, account); System.out.println(account.getEmails()); 上記のコードを実行すると以下のように出力される。 .. code-block:: console [a@example.com, d@example.com, e@example.com, a@example.com, b@example.com, c@example.com] コピー元BeanのCollectionの全要素が、コピー先BeanのCollectionに追加されている。 \ ``a@exmample.com``\ をもつ2つの\ ``Email``\ オブジェクトは"等価"であるが、単純に追加される。 (ここでいう"等価"とは\ ``Email.equals`` で比較すると\ ``true``\ になり、\ ``Email.hashCode``\ の値も同じであることを意味する。) 上記の振る舞いは、Dozerの用語では\ **cumulative**\ と呼ばれ、Collectionをマッピングする際のデフォルトの挙動となっている。 この挙動はマッピング定義XMLファイルにおいて変更することができる。 .. code-block:: xml :emphasize-lines: 9 com.example.dozer.AccountForm com.example.dozer.Account emails emails .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ````\ タグの\ ``relationship-type``\ 属性に\ ``non-cumulative``\ を指定する。デフォルト値は\ ``cumulative``\ である。 | | マッピング対象のBeanの全フィールドに対して\ ``non-cumulative``\ を指定したい場合は、\ ````\ タグの\ ``relationship-type``\ 属性に\ ``non-cumulative``\ を指定することもできる。 この設定のもと、前述のコードを実行すると以下のように出力される。 .. code-block:: console [a@example.com, d@example.com, e@example.com, b@example.com, c@example.com] 等価であるオブジェクトの重複がなくなっていることが分かる。 .. note:: 変換元のオブジェクトが、変換先のオブジェクトで更新されることに注意されたい。 上記の例では\ ``AccountForm``\ の中の\ ``a@exmample.com``\ がコピー先に格納される。 .. figure:: ./images/dozer_noncumulativeupdate.png :alt: noncumulative update using dozer :width: 60% コピー先のコレクションにのみに存在する項目は除外したい場合も、マッピング定義XMLファイルの設定で実現することができる。 .. code-block:: xml :emphasize-lines: 9 com.example.dozer.AccountForm com.example.dozer.Account emails emails .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ````\ タグの\ ``remove-orphans``\ 属性に\ ``true``\ を設定する。デフォルト値は\ ``false``\ である。 この設定のもと、前述のコードを実行すると以下のように出力される。 .. code-block:: console [a@example.com, b@example.com, c@example.com] コピー元にあるオブジェクトだけがコピー先のコレクション内に残っていることが分かる。 いかのように設定しても同じ結果が得られる。 .. code-block:: xml :emphasize-lines: 9 com.example.dozer.AccountForm com.example.dozer.Account emails emails .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ````\ タグの\ ``copy-by-reference``\ 属性に\ ``true``\ を設定する。デフォルト値は\ ``false``\ である。 これまでの挙動を図で表現する。 * デフォルトの挙動(cumulative) .. figure:: ./images/dozer-collection-cumulative.png :width: 60% * non-cumulative .. figure:: ./images/dozer-collection-non-cumulative.png :width: 60% * non-cumulativeかつremove-orphans=true .. figure:: ./images/dozer-collection-non-cumulative-and-orphan-remove.png :width: 60% copy-by-referenceもこのパターンである。 .. note:: 「non-cumulativeかつremove-orphans=true」のパターンと「copy-by-reference」のパターンの違いは、Bean変換後のCollectionのコンテナがコピー先のものか、コピー元のものかで異なる点である。 「non-cumulativeかつremove-orphans=true」のパターンの場合は、Bean変換後のCollectionのコンテナはコピー先のものであり、「copy-by-reference」のパターンはコピー元のものである。 以下に図で説明する。 * non-cumulativeかつremove-orphans=true .. figure:: ./images/dozer-differrence1.png :width: 50% * copy-by-reference .. figure:: ./images/dozer-differrence2.png :width: 50% \ **コピー先がJPA (Hibernate)のエンティティで1対多や多対多の関連を持つ場合は要注意である**\ 。コピー先のエンティティがEntityManagerの管理下にある場合、予期せぬトラブルに遭うことがある。 例えばコレクションのコンテナが変更されると全件DELETE + 全件INSERTのSQLが発行され、「non-cumulativeかつremove-orphans=true」でコピーした場合は変更内容をUPDATE(要素数が異なる場合はDELETE or INSERT)のSQLが発行される場合がある。 どちらが良いかは要件次第である。 .. warning:: マッピング対象のBeanが\ ``String``\ のコレクションを持つ場合、\ `期待通りの挙動にならないバグ `_\ がある。 .. code-block:: java StringListSrc src = new StringListSrc; StringListDest dest = new StringListDest(); List stringsSrc = new ArrayList(); List stringsDest = new ArrayList(); stringsSrc.add("a"); stringsSrc.add("b"); stringsSrc.add("c"); stringsDest.add("a"); stringsDest.add("d"); stringsDest.add("e"); src.setStrings(stringsSrc); dest.setStrings(stringsDest); beanMapper.map(src, dest); System.out.println(dest.getStrings()); 上記のコードをnon-cumulativeかつremove-orphans=trueの設定で実行すると、 .. code-block:: console [a, b, c] と出力されることを期待するが、実際には .. code-block:: console [b, c] と出力され、\ **重複したStringが除かれてしまう**\ 。 copy-by-reference="true"の設定で実行すると、期待通り .. code-block:: console [a, b, c] と出力される。 .. tip:: Dozerでは、Genericsを使用しないリスト間でもマッピングできる。このとき、変換元と変換先に含まれているオブジェクトのデータ型をHINTとして指定できる。 詳細は、 `Dozerの公式マニュアル -Collection and Array Mapping(Using Hints for Collection Mapping)- `_ を参照されたい。 How to extend ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. _how-to-make-customconverter-label: カスタムコンバーターの作成 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" | Dozerがサポートしていないデータ型のマッピングでは、同じ型同士の場合も異なる型の場合も、カスタムコンバーター経由でマッピングできる。 * 例 : \ ``java.lang.String``\ <=> \ ``org.joda.time.DateTime``\ | カスタムコンバーターは、Dozerが提供している\ ``com.github.dozermapper.core.CustomConverter``\ を実装したクラスである。 | カスタムコンバーターの指定は、以下3パターンで行える。 * Global Configuration * クラスレベル * フィールドレベル アプリケーション全体で、同様のロジックにより変換を行いたい場合は、Global Configurationを推奨する。 カスタムコンバーターを実装する場合は\ ``com.github.dozermapper.core.DozerConverter``\ を継承するのが便利である。 .. code-block:: java package com.example.yourproject.common.bean.converter; import com.github.dozermapper.core.DozerConverter; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.springframework.util.StringUtils; public class StringToJodaDateTimeConverter extends DozerConverter { // (1) public StringToJodaDateTimeConverter() { super(String.class, DateTime.class); // (2) } @Override public DateTime convertTo(String source, DateTime destination) {// (3) if (!StringUtils.hasLength(source)) { return null; } DateTimeFormatter formatter = DateTimeFormat .forPattern("yyyy-MM-dd HH:mm:ss"); DateTime dt = formatter.parseDateTime(source); return dt; } @Override public String convertFrom(DateTime source, String destination) {// (4) if (source == null) { return null; } return source.toString("yyyy-MM-dd HH:mm:ss"); } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``com.github.dozermapper.core.DozerConverter``\ を継承する。 * - | (2) - | コンストラクタで対象の2つのクラスを設定する。 * - | (3) - | \ ``String``\ から\ ``DateTime``\ の変換ロジックを記述する。本例ではデフォルトLocaleを使用する。 * - | (4) - | \ ``DateTime``\ から\ ``String``\ の変換ロジックを記述する。本例ではデフォルトLocaleを使用する。 作成したカスタムコンバーターを、マッピングに利用するために定義する必要がある。 dozer-configration-mapping.xml .. code-block:: xml java.lang.String org.joda.time.DateTime .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | すべてのカスタムコンバーターが属する、\ ``custom-converters``\ を定義する。 * - | (2) - | 個別の変換の行うconverterを定義する。converterのタイプに、実装クラスの完全修飾クラス名(FQCN)を指定する。 * - | (3) - | 変換元Beanの完全修飾クラス名(FQCN) * - | (4) - | 変換先Beanの完全修飾クラス名(FQCN) 上記のマッピングを行ったことで、アプリケーション全体で、\ ``java.lang.String``\ <=> \ ``org.joda.time.DateTime``\ の変換が必要な場合、標準のマッピングではなく、カスタムコンバーター呼び出しでマッピングが行われる。 例 : 変換元のBean定義 .. code-block:: java public class Source { private int id; private String date; // omitted setter/getter } 変換先のBean定義 .. code-block:: java public class Destination { private int id; private DateTime date; // omitted setter/getter } マッピング (双方向例) .. code-block:: java Source source = new Source(); source.setId(1); source.setDate("2012-08-10 23:12:12"); DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); DateTime dt = formatter.parseDateTime(source.getDate()); // Source to Destination Bean Mapping (String to org.joda.time.DateTime) Destination destination = dozerBeanMapper.map(source, Destination.class); assertThat(destination.getId(), is(1)); assertThat(destination.getDate(),is(dt)); // Destination to Source Bean Mapping (org.joda.time.DateTime to String) dozerBeanMapper.map(destination, source); assertThat(source.getId(), is(1)); assertThat(source.getDate(),is("2012-08-10 23:12:12")); カスタムコンバーターに関する詳細は、 `Dozerの公式マニュアル -Custom Converters- `_ を参照されたい。 .. note:: \ ``String``\ から\ ``java.util.Date``\ など標準の日付・時刻オブジェクトへの変換については"\ :ref:`beanconverter-string-and-datetime`\"で述べる。 Appendix ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ マッピング定義XMLファイルで指定できるオプションを説明する。 すべてのオプションは、 `Dozerの公式マニュアル -Custom Mappings Via Dozer XML Files- `_ で確認できる。 .. _fieldexclude: フィールド除外設定 (field-exclude) """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" Beanを変換する際に、コピーしてほしくないフィールドを除外することができる。 以下のようなBeanの変換を考える。 変換元のBean定義 .. code-block:: java public class Source { private int id; private String name; private String title; // omitted setter/getter } コピー先のBean定義 .. code-block:: java public class Destination { private int id; private String name; private String title; // omitted setter/getter } コピー元のBeanから任意のフィールドをマッピングから除外したい場合は以下のように定義する。 フィールド除外の設定は、マッピング定義XMLファイルで、以下のように行う。 .. code-block:: xml com.xx.xx.Source com.xx.xx.Destination title title .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | 除外したいフィールドを、要素で設定する。この例の場合、指定した上でmapメソッドを実行すると、SourceオブジェクトからDestinationオブジェクトをコピーする際に、destinationのtitleの値が、上書きされない。 .. code-block:: java Source source = new Source(); source.setId(1); source.setName("SourceName"); source.setTitle("SourceTitle"); Destination destination = new Destination(); destination.setId(2); destination.setName("DestinationName"); destination.setTitle("DestinationTitle"); beanMapper.map(source, destination); System.out.println(destination.getId()); System.out.println(destination.getName()); System.out.println(destination.getTitle()); 上記のコードを実行すると以下のように出力される。 .. code-block:: console 1 SourceName DestinationTitle マッピング後、destinationのtitleの値は、前の状態のままである。 | マッピングの特定化 (map-id) """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" \ :ref:`fieldexclude`\ で示したマッピングは、アプリケーション全体でBean変換する際に適用される。 マッピングの適用範囲を制限(特定化)したい場合は、以下のように、map-idを指定して定義する。 .. code-block:: xml com.xx.xx.Source com.xx.xx.Destination title title 上記の設定を行うと、\ ``map``\ メソッドにmap-id(mapidTitleFieldExclude)を渡すことでtitleのコピーを除外できる。 map-idを指定しない場合はこの設定は適用されず、全フィールドがコピーされる。 \ ``map``\ メソッドにmap-idを渡す例を、以下に示す。 .. code-block:: java Source source = new Source(); source.setId(1); source.setName("SourceName"); source.setTitle("SourceTitle"); Destination destination1 = new Destination(); destination1.setId(2); destination1.setName("DestinationName"); destination1.setTitle("DestinationTitle"); beanMapper.map(source, destination1); // (1) System.out.println(destination1.getId()); System.out.println(destination1.getName()); System.out.println(destination1.getTitle()); Destination destination2 = new Destination(); destination2.setId(2); destination2.setName("DestinationName"); destination2.setTitle("DestinationTitle"); beanMapper.map(source, destination2, "mapidTitleFieldExclude"); // (2) System.out.println(destination2.getId()); System.out.println(destination2.getName()); System.out.println(destination2.getTitle()); .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | 通常のマッピング。 * - | (2) - | 第三引数にmap-idを渡し、特定のマッピングルールを適用する。 上記のコードを実行すると以下のように出力される。 .. code-block:: console 1 SourceName SourceTitle 1 SourceName DestinationTitle | .. tip:: map-idの指定は、mapping項目だけでなく、フィールドの定義でも行える。 詳細は、 `Dozerの公式マニュアル -Context Based Mapping- `_ を参照されたい。 | .. note:: Webアプリケーションにおいて、新規追加・更新両方の操作で同じフォームオブジェクトを使う場合がある。 このとき、フォームオブジェクトをドメインオブジェクトにコピー(マップ)する上で、操作によってはコピーしたくないフィールドもある。 この場合に、\ ````\ を使用する。 * 例:新規作成のフォームではuserIdを含むが、更新用のフォームではuserIdを含まない。 この場合に同じフォームオブジェクトを使用すると、更新時にuserIdにnullが設定される。コピー先のオブジェクトをDBから取得して、 フォームオブジェクトをそのままコピーすると、コピー先のuserIdまでnullとなる。これを回避するために、 更新用のmap-idを用意し、更新時はuserIdに対して、フィールド除外の設定を行う。 | コピー元のnull・空フィールドを除外する設定 (map-null, map-empty) """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" コピー元のBeanのフィールドが、\ ``null``\ の場合、あるいは空の場合に、マッピングから除外することができる。 以下のように、マッピング定義XMLファイルに設定する。 .. code-block:: xml com.xx.xx.Source com.xx.xx.Destination .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | コピー元のBeanのフィールドが\ ``null``\ の場合にマッピングから除外したい場合は\ ``map-null``\ 属性に\ ``false``\ を設定する。デフォルト値は\ ``true``\ である。 | 空の場合に、マッピングから除外したい場合は\ ``map-empty-string``\ 属性に\ ``false``\ を設定する。デフォルト値は\ ``true``\ である。 変換元のBean定義 .. code-block:: java public class Source { private int id; private String name; private String title; // omitted setter/getter } 変換先のBean定義 .. code-block:: java public class Destination { private int id; private String name; private String title; // omitted setter/getter } マッピング例 .. code-block:: java Source source = new Source(); source.setId(1); source.setName(null); source.setTitle(""); Destination destination = new Destination(); destination.setId(2); destination.setName("DestinationName"); destination.setTitle("DestinationTitle"); beanMapper.map(source, destination); System.out.println(destination.getId()); System.out.println(destination.getName()); System.out.println(destination.getTitle()); 上記のコードを実行すると以下のように出力される。 .. code-block:: console 1 DestinationName DestinationTitle コピー元Beanの\ ``name``\ と\ ``title``\ フィールドは、\ ``null``\ 、あるいは空で、マッピングから除外されている。 .. _beanconverter-string-and-datetime: 文字列から日付・時刻オブジェクトへのマッピング """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" コピー元の文字列型のフィールドを、コピー先の日付・時刻系のフィールドにマッピングできる。 以下の変換をサポートしている。 日付・時刻系 * \ ``java.lang.String``\ <=> \ ``java.util.Date``\ * \ ``java.lang.String``\ <=> \ ``java.util.Calendar``\ * \ ``java.lang.String``\ <=> \ ``java.util.GregorianCalendar``\ * \ ``java.lang.String``\ <=> \ ``java.sql.Timestamp``\ * \ ``java.lang.String``\ <=> \ ``java.time.LocalDateTime``\ * \ ``java.lang.String``\ <=> \ ``java.time.OffsetDateTime``\ * \ ``java.lang.String``\ <=> \ ``java.time.ZonedDateTime``\ 日付のみ * \ ``java.lang.String``\ <=> \ ``java.sql.Date``\ * \ ``java.lang.String``\ <=> \ ``java.time.LocalDate``\ 時刻のみ * \ ``java.lang.String``\ <=> \ ``java.sql.Time``\ * \ ``java.lang.String``\ <=> \ ``java.time.LocalTime``\ * \ ``java.lang.String``\ <=> \ ``java.time.OffsetTime``\ | 日付・時刻系の変換は、以下のように行う。 | 例として、\ ``java.time.LocalDateTime``\ への変換を説明する。 .. code-block:: xml com.xx.xx.Source com.xx.xx.Destination date date .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | コピー元のフィールド名と日付形式を指定する。 変換元のBean定義 .. code-block:: java public class Source { private String date; // omitted setter/getter } 変換先のBean定義 .. code-block:: java public class Destination { private LocalDateTime date; // omitted setter/getter } マッピング .. code-block:: java Source source = new Source(); source.setDate("2013-10-10 11:11:11.111"); Destination destination = beanMapper.map(source, Destination.class); assert(destination.getDate().equals(LocalDateTime.parse("2013-10-10 11:11:11.111", DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss.SSS")))); | 日付形式は、個別のマッピング定義毎に設定するよりも、プロジェクトで一括して設定したいケースが多い。 | その場合はDozerのGlobal configurationファイルで設定することを推奨する。 | その場合、アプリケーション全体のマッピングで設定された日付形式が、適用される。 .. code-block:: xml uuuu-MM-dd HH:mm:ss.SSS | ファイル名には制限はないが、src/main/resources/META-INF/dozer/dozer-configration-mapping.xmlを推奨する。 | dozer-configration-mapping.xml内の設定の範囲は、この設定ファイル内でアプリケーション全体に影響を与える、Global Configurationを行えばよい。 設定可能な項目の詳細について、 `Dozerの公式マニュアル -Global Configuration- `_ を参照されたい。 .. warning:: \ ``java.util.Date``\と\ ``java.time.LocalDate``\を併用するようなアプリケーションのとき、年形式に\ ``uuuu``\と\ ``yyyy``\を使い分ける必要があるため、アプリケーション全体で設定すると困るケースがある。このような場合では、アプリケーション全体の設定に加えて個別のマッピング定義で日付形式を設定すれば対応可能である。 .. note:: Java SE 11ではJava SE 8と日付の文字列表現が異なる場合がある。 Java SE 8と同様に表現するには\ :ref:`change-default-locale--data-from-java9`\ を参照されたい。 .. _beanconverter-mapping-error: マッピングのエラー """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" マッピング中にマッピング処理が失敗したら、\ ``com.github.dozermapper.core.MappingException``\ (実行時例外)がスローされる。 \ ``MappingException`` \がスローされる代表的な例を、以下に挙げる。 * \ ``map``\ メソッドに存在しないmap-idが渡されている。 * \ ``map``\ メソッドに存在するmap-idを渡したが、マップ処理に渡したソース・ターゲット型は、そのmap-idに指定している定義とは異なる。 * Dozerがサポートしていない変換の場合、かつ、その変換用のカスタムコンバーターも存在しない場合。 これらは通常プログラムバグであるので、\ ``map``\ メソッドの呼び出しの部分を正しく修正する必要がある。 .. warning:: Dozer 6.3.0から、マッピング定義XMLファイルの解析にデフォルトでJAXBが利用されるようになった。 これにより、Dozer 6.2.0以前では無視されていたマッピング定義XMLファイルのコンテンツ部の両端に存在する改行コードは、Dozer 6.3.0以降では値として読み取られるようになった。 マッピング定義XMLファイルのコンテンツ部の両端に改行コードが存在する場合、指定されたフィールド名が正しく認識されない等の不具合が生じる可能性があるため、注意されたい。 .. raw:: latex \newpage