オリカとのマッピング
1. 概要
Orikaは、recursively copies data from one object to anotherであるJavaBeanマッピングフレームワークです。 多層アプリケーションを開発するときに非常に役立ちます。
これらのレイヤー間でデータオブジェクトを前後に移動するとき、異なるAPIに対応するためにオブジェクトをあるインスタンスから別のインスタンスに変換する必要があることを発見するのが一般的です。
これを実現するいくつかの方法は次のとおりです:hard coding the copying logic or to implement bean mappers like Dozer。 ただし、1つのオブジェクトレイヤーと別のオブジェクトレイヤーとの間のマッピングプロセスを簡素化するために使用できます。
Orikauses byte code generation to create fast mappersは最小限のオーバーヘッドで、Dozerのような他の反射ベースのマッパーよりもはるかに高速です。
2. 簡単な例
マッピングフレームワークの基本的な基礎は、MapperFactoryクラスです。 これは、マッピングを構成し、実際のマッピング作業を実行するMapperFacadeインスタンスを取得するために使用するクラスです。
次のようにMapperFactoryオブジェクトを作成します。
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
次に、2つのフィールドを持つソースデータオブジェクトSource.javaがあると仮定します。
public class Source {
private String name;
private int age;
public Source(String name, int age) {
this.name = name;
this.age = age;
}
// standard getters and setters
}
そして、同様の宛先データオブジェクト、Dest.java:
public class Dest {
private String name;
private int age;
public Dest(String name, int age) {
this.name = name;
this.age = age;
}
// standard getters and setters
}
これは、Orikaを使用した最も基本的なBeanマッピングです。
@Test
public void givenSrcAndDest_whenMaps_thenCorrect() {
mapperFactory.classMap(Source.class, Dest.class);
MapperFacade mapper = mapperFactory.getMapperFacade();
Source src = new Source("example", 10);
Dest dest = mapper.map(src, Dest.class);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
ご覧のとおり、マッピングするだけで、Sourceと同じフィールドを持つDestオブジェクトを作成しました。 デフォルトでは、双方向マッピングまたは逆マッピングも可能です。
@Test
public void givenSrcAndDest_whenMapsReverse_thenCorrect() {
mapperFactory.classMap(Source.class, Dest.class).byDefault();
MapperFacade mapper = mapperFactory.getMapperFacade();
Dest src = new Dest("example", 10);
Source dest = mapper.map(src, Source.class);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
3. Mavenセットアップ
MavenプロジェクトでOrikaマッパーを使用するには、pom.xmlにorika-coreの依存関係が必要です。
ma.glasnost.orika
orika-core
1.4.6
最新バージョンは常にhereで見つけることができます。
3. MapperFactoryの操作
Orikaを使用したマッピングの一般的なパターンには、MapperFactoryオブジェクトの作成、デフォルトのマッピング動作を微調整する必要がある場合に備えた構成、オブジェクトからのMapperFacadeオブジェクトの取得、最後に実際のマッピングが含まれます。
すべての例でこのパターンを観察します。 ただし、最初の例では、マッパーのデフォルトの動作を示しており、私たちの側からの調整はありません。
3.1. BoundMapperFacadeとMapperFacade
注意すべきことの1つは、非常に遅いデフォルトのMapperFacadeよりもBoundMapperFacadeを使用することを選択できることです。 これらは、マッピングする特定のタイプのペアがある場合です。
したがって、最初のテストは次のようになります。
@Test
public void givenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect() {
BoundMapperFacade
boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
Source src = new Source("example", 10);
Dest dest = boundMapper.map(src);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
ただし、BoundMapperFacadeを双方向にマップするには、デフォルトのMapperFacadeの場合に調べたmapメソッドではなく、mapReverseメソッドを明示的に呼び出す必要があります。
@Test
public void givenSrcAndDest_whenMapsUsingBoundMapperInReverse_thenCorrect() {
BoundMapperFacade
boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
Dest src = new Dest("example", 10);
Source dest = boundMapper.mapReverse(src);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
そうでない場合、テストは失敗します。
3.2. フィールドマッピングの構成
これまで見てきた例では、同じフィールド名を持つソースクラスと宛先クラスが関係しています。 このサブセクションでは、2つの間に違いがある場合に対処します。
name、nickname、ageの3つのフィールドを持つソースオブジェクトPersonについて考えてみます。
public class Person {
private String name;
private String nickname;
private int age;
public Person(String name, String nickname, int age) {
this.name = name;
this.nickname = nickname;
this.age = age;
}
// standard getters and setters
}
次に、アプリケーションの別のレイヤーに同様のオブジェクトがありますが、フランスのプログラマーが作成します。 これがPersonneと呼ばれ、フィールドnom、surnom、ageがあり、すべて上記の3つに対応しているとします。
public class Personne {
private String nom;
private String surnom;
private int age;
public Personne(String nom, String surnom, int age) {
this.nom = nom;
this.surnom = surnom;
this.age = age;
}
// standard getters and setters
}
Orikaはこれらの違いを自動的に解決することはできません。 ただし、ClassMapBuilderAPI to register these unique mappings.を使用できます
以前に使用しましたが、その強力な機能はまだ利用していません。 デフォルトのMapperFacadeを使用した前述の各テストの最初の行は、ClassMapBuilderAPI to register the two classes we wanted to map:を使用していました。
mapperFactory.classMap(Source.class, Dest.class);
デフォルトの設定を使用してすべてのフィールドをマッピングし、より明確にすることもできます。
mapperFactory.classMap(Source.class, Dest.class).byDefault()
byDefault()メソッド呼び出しを追加することにより、ClassMapBuilderAPI.を使用してマッパーの動作を既に構成しています。
ここで、PersonneをPersonにマップできるようにしたいので、ClassMapBuilderAPI:を使用してマッパーへのフィールドマッピングも構成します。
@Test
public void givenSrcAndDestWithDifferentFieldNames_whenMaps_thenCorrect() {
mapperFactory.classMap(Personne.class, Person.class)
.field("nom", "name").field("surnom", "nickname")
.field("age", "age").register();
MapperFacade mapper = mapperFactory.getMapperFacade();
Personne frenchPerson = new Personne("Claire", "cla", 25);
Person englishPerson = mapper.map(frenchPerson, Person.class);
assertEquals(englishPerson.getName(), frenchPerson.getNom());
assertEquals(englishPerson.getNickname(), frenchPerson.getSurnom());
assertEquals(englishPerson.getAge(), frenchPerson.getAge());
}
構成をMapperFactoryに登録するには、register()APIメソッドを呼び出すことを忘れないでください。
1つのフィールドのみが異なる場合でも、このルートをたどると、両方のオブジェクトで同じageを含むallフィールドマッピングを明示的に登録する必要があります。そうしないと、未登録のフィールドがマッピングされず、テストが行われます。不合格。
これはすぐに面倒になります、what if we only want to map one field out of 20、すべてのマッピングを構成する必要がありますか?
いいえ、マッピングを明示的に定義していない場合に、マッパーにデフォルトのマッピング構成を使用するように指示したときではありません。
mapperFactory.classMap(Personne.class, Person.class)
.field("nom", "name").field("surnom", "nickname").byDefault().register();
ここでは、ageフィールドのマッピングを定義していませんが、それでもテストは合格します。
3.3. フィールドを除外する
Personneのnomフィールドをマッピングから除外したいとすると、Personオブジェクトは、除外されていないフィールドの新しい値のみを受け取ります。
@Test
public void givenSrcAndDest_whenCanExcludeField_thenCorrect() {
mapperFactory.classMap(Personne.class, Person.class).exclude("nom")
.field("surnom", "nickname").field("age", "age").register();
MapperFacade mapper = mapperFactory.getMapperFacade();
Personne frenchPerson = new Personne("Claire", "cla", 25);
Person englishPerson = mapper.map(frenchPerson, Person.class);
assertEquals(null, englishPerson.getName());
assertEquals(englishPerson.getNickname(), frenchPerson.getSurnom());
assertEquals(englishPerson.getAge(), frenchPerson.getAge());
}
MapperFactoryの構成でそれを除外する方法に注意してください。次に、Personオブジェクトのnameの値がnullのままであると予想される最初のアサーションにも注意してください。マッピングから除外された結果。
4. コレクションのマッピング
ソースオブジェクトがコレクション内のすべてのプロパティを維持している間に、宛先オブジェクトに一意の属性がある場合があります。
4.1. リストと配列
1つのフィールド、つまり人の名前のリストしかないソースデータオブジェクトについて考えてみます。
public class PersonNameList {
private List nameList;
public PersonNameList(List nameList) {
this.nameList = nameList;
}
}
ここで、firstNameとlastNameを別々のフィールドに分割する宛先データオブジェクトについて考えてみます。
public class PersonNameParts {
private String firstName;
private String lastName;
public PersonNameParts(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
インデックス0には常に人のfirstNameがあり、インデックス1には常にlastNameがあると確信しているとしましょう。
Orikaでは、ブラケット表記を使用してコレクションのメンバーにアクセスできます。
@Test
public void givenSrcWithListAndDestWithPrimitiveAttributes_whenMaps_thenCorrect() {
mapperFactory.classMap(PersonNameList.class, PersonNameParts.class)
.field("nameList[0]", "firstName")
.field("nameList[1]", "lastName").register();
MapperFacade mapper = mapperFactory.getMapperFacade();
List nameList = Arrays.asList(new String[] { "Sylvester", "Stallone" });
PersonNameList src = new PersonNameList(nameList);
PersonNameParts dest = mapper.map(src, PersonNameParts.class);
assertEquals(dest.getFirstName(), "Sylvester");
assertEquals(dest.getLastName(), "Stallone");
}
PersonNameListの代わりにPersonNameArrayがあったとしても、同じテストが名前の配列に合格します。
4.2. Maps
ソースオブジェクトに値のマップがあると仮定します。 そのマップにキーfirstがあり、その値は宛先オブジェクト内の人のfirstNameを表します。
同様に、同じマップに別のキーlastがあり、その値が宛先オブジェクト内の人のlastNameを表していることがわかります。
public class PersonNameMap {
private Map nameMap;
public PersonNameMap(Map nameMap) {
this.nameMap = nameMap;
}
}
前のセクションの場合と同様に、ブラケット表記を使用しますが、インデックスを渡す代わりに、指定された宛先フィールドに値をマップするキーを渡します。
Orikaはキーを取得する2つの方法を受け入れます。どちらも次のテストで表されます。
@Test
public void givenSrcWithMapAndDestWithPrimitiveAttributes_whenMaps_thenCorrect() {
mapperFactory.classMap(PersonNameMap.class, PersonNameParts.class)
.field("nameMap['first']", "firstName")
.field("nameMap[\"last\"]", "lastName")
.register();
MapperFacade mapper = mapperFactory.getMapperFacade();
Map nameMap = new HashMap<>();
nameMap.put("first", "Leornado");
nameMap.put("last", "DiCaprio");
PersonNameMap src = new PersonNameMap(nameMap);
PersonNameParts dest = mapper.map(src, PersonNameParts.class);
assertEquals(dest.getFirstName(), "Leornado");
assertEquals(dest.getLastName(), "DiCaprio");
}
一重引用符または二重引用符を使用できますが、後者はエスケープする必要があります。
5. ネストされたフィールドのマップ
前述のコレクションの例に続いて、ソースデータオブジェクト内に、マッピングする値を保持する別のデータ転送オブジェクト(DTO)があると仮定します。
public class PersonContainer {
private Name name;
public PersonContainer(Name name) {
this.name = name;
}
}
public class Name {
private String firstName;
private String lastName;
public Name(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
ネストされたDTOのプロパティにアクセスして目的のオブジェクトにマップできるようにするには、次のようにドット表記を使用します。
@Test
public void givenSrcWithNestedFields_whenMaps_thenCorrect() {
mapperFactory.classMap(PersonContainer.class, PersonNameParts.class)
.field("name.firstName", "firstName")
.field("name.lastName", "lastName").register();
MapperFacade mapper = mapperFactory.getMapperFacade();
PersonContainer src = new PersonContainer(new Name("Nick", "Canon"));
PersonNameParts dest = mapper.map(src, PersonNameParts.class);
assertEquals(dest.getFirstName(), "Nick");
assertEquals(dest.getLastName(), "Canon");
}
6. ヌル値のマッピング
場合によっては、nullが検出されたときにマップするか無視するかを制御できます。 デフォルトでは、Orikaは次の場合にnull値をマップします。
@Test
public void givenSrcWithNullField_whenMapsThenCorrect() {
mapperFactory.classMap(Source.class, Dest.class).byDefault();
MapperFacade mapper = mapperFactory.getMapperFacade();
Source src = new Source(null, 10);
Dest dest = mapper.map(src, Dest.class);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
この動作は、どの程度具体的になりたいかに応じて、さまざまなレベルでカスタマイズできます。
6.1. グローバル構成
グローバルMapperFactoryを作成する前に、nullをマップするか、グローバルレベルで無視するようにマッパーを構成できます。 最初の例でこのオブジェクトを作成した方法を覚えていますか? 今回は、ビルドプロセス中に追加の呼び出しを追加します。
MapperFactory mapperFactory = new DefaultMapperFactory.Builder()
.mapNulls(false).build();
テストを実行して、実際にnullがマップされていないことを確認できます。
@Test
public void givenSrcWithNullAndGlobalConfigForNoNull_whenFailsToMap_ThenCorrect() {
mapperFactory.classMap(Source.class, Dest.class);
MapperFacade mapper = mapperFactory.getMapperFacade();
Source src = new Source(null, 10);
Dest dest = new Dest("Clinton", 55);
mapper.map(src, dest);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), "Clinton");
}
何が起こるかというと、デフォルトではヌルがマップされます。 これは、ソースオブジェクトのフィールド値がnullであり、宛先オブジェクトの対応するフィールドの値に意味のある値がある場合でも、上書きされることを意味します。
この場合、対応するソースフィールドにnull値がある場合、宛先フィールドは上書きされません。
6.2. ローカル構成
null値のマッピングは、mapNulls(true|false)またはmapNullsInReverse(true|false)を使用して逆方向のヌルのマッピングを制御することにより、ClassMapBuilderで制御できます。
ClassMapBuilderインスタンスにこの値を設定すると、値が設定された後、同じClassMapBuilderで作成されたすべてのフィールドマッピングが同じ値になります。
テストの例でこれを説明しましょう。
@Test
public void givenSrcWithNullAndLocalConfigForNoNull_whenFailsToMap_ThenCorrect() {
mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
.mapNulls(false).field("name", "name").byDefault().register();
MapperFacade mapper = mapperFactory.getMapperFacade();
Source src = new Source(null, 10);
Dest dest = new Dest("Clinton", 55);
mapper.map(src, dest);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), "Clinton");
}
nameフィールドを登録する直前にmapNullsを呼び出す方法に注意してください。これにより、null値がある場合、mapNulls呼び出しに続くすべてのフィールドが無視されます。
双方向マッピングは、マッピングされたnull値も受け入れます。
@Test
public void givenDestWithNullReverseMappedToSource_whenMapsByDefault_thenCorrect() {
mapperFactory.classMap(Source.class, Dest.class).byDefault();
MapperFacade mapper = mapperFactory.getMapperFacade();
Dest src = new Dest(null, 10);
Source dest = new Source("Vin", 44);
mapper.map(src, dest);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
また、mapNullsInReverseを呼び出してfalseを渡すことで、これを防ぐことができます。
@Test
public void
givenDestWithNullReverseMappedToSourceAndLocalConfigForNoNull_whenFailsToMap_thenCorrect() {
mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
.mapNullsInReverse(false).field("name", "name").byDefault()
.register();
MapperFacade mapper = mapperFactory.getMapperFacade();
Dest src = new Dest(null, 10);
Source dest = new Source("Vin", 44);
mapper.map(src, dest);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), "Vin");
}
6.3. フィールドレベルの構成
次のように、fieldMapを使用してフィールドレベルでこれを構成できます。
mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
.fieldMap("name", "name").mapNulls(false).add().byDefault().register();
この場合、構成は、フィールドレベルで呼び出したnameフィールドにのみ影響します。
@Test
public void givenSrcWithNullAndFieldLevelConfigForNoNull_whenFailsToMap_ThenCorrect() {
mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
.fieldMap("name", "name").mapNulls(false).add().byDefault().register();
MapperFacade mapper = mapperFactory.getMapperFacade();
Source src = new Source(null, 10);
Dest dest = new Dest("Clinton", 55);
mapper.map(src, dest);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), "Clinton");
}
7. Orikaカスタムマッピング
これまで、ClassMapBuilderAPI. We shall still use the same API but customize our mapping using Orika’s CustomMapper class.を使用した簡単なカスタムマッピングの例を見てきました。
それぞれがdtobと呼ばれる特定のフィールドを持つ2つのデータオブジェクトがあると仮定します。これは、人の誕生の日付と時刻を表します。
1つのデータオブジェクトは、この値を次のISO形式のdatetime Stringとして表します。
2007-06-26T21:22:39Z
もう1つは、次のUNIXタイムスタンプ形式のlongタイプと同じものを表します。
1182882159000
明らかに、これまでに説明したカスタマイズのいずれも、マッピングプロセス中に2つの形式間で変換するのに十分ではなく、Orikaの組み込みコンバーターでさえジョブを処理できません。 これは、マッピング中に必要な変換を行うためにCustomMapperを記述する必要がある場所です。
最初のデータオブジェクトを作成しましょう。
public class Person3 {
private String name;
private String dtob;
public Person3(String name, String dtob) {
this.name = name;
this.dtob = dtob;
}
}
次に、2番目のデータオブジェクト:
public class Personne3 {
private String name;
private long dtob;
public Personne3(String name, long dtob) {
this.name = name;
this.dtob = dtob;
}
}
CustomMapperを使用すると双方向マッピングに対応できるため、現時点では、どちらが送信元でどちらが宛先であるかをラベル付けしません。
CustomMapper抽象クラスの具体的な実装は次のとおりです。
class PersonCustomMapper extends CustomMapper {
@Override
public void mapAtoB(Personne3 a, Person3 b, MappingContext context) {
Date date = new Date(a.getDtob());
DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
String isoDate = format.format(date);
b.setDtob(isoDate);
}
@Override
public void mapBtoA(Person3 b, Personne3 a, MappingContext context) {
DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
Date date = format.parse(b.getDtob());
long timestamp = date.getTime();
a.setDtob(timestamp);
}
};
メソッドmapAtoBとmapBtoAを実装していることに注意してください。 両方を実装すると、マッピング機能が双方向になります。
Each method exposes the data objects we are mapping and we take care of copying the field values from one to the other。
そこに、目的のオブジェクトに書き込む前に要件に従ってソースデータを操作するカスタムコードを記述する場所があります。
テストを実行して、カスタムマッパーが機能することを確認しましょう。
@Test
public void givenSrcAndDest_whenCustomMapperWorks_thenCorrect() {
mapperFactory.classMap(Personne3.class, Person3.class)
.customize(customMapper).register();
MapperFacade mapper = mapperFactory.getMapperFacade();
String dateTime = "2007-06-26T21:22:39Z";
long timestamp = new Long("1182882159000");
Personne3 personne3 = new Personne3("Leornardo", timestamp);
Person3 person3 = mapper.map(personne3, Person3.class);
assertEquals(person3.getDtob(), dateTime);
}
ClassMapBuilderAPI, just like all other simple customizations.を介してカスタムマッパーをOrikaのマッパーに渡すことに注意してください。
双方向マッピングが機能することも確認できます。
@Test
public void givenSrcAndDest_whenCustomMapperWorksBidirectionally_thenCorrect() {
mapperFactory.classMap(Personne3.class, Person3.class)
.customize(customMapper).register();
MapperFacade mapper = mapperFactory.getMapperFacade();
String dateTime = "2007-06-26T21:22:39Z";
long timestamp = new Long("1182882159000");
Person3 person3 = new Person3("Leornardo", dateTime);
Personne3 personne3 = mapper.map(person3, Personne3.class);
assertEquals(person3.getDtob(), timestamp);
}
8. 結論
この記事では、explored the most important features of the Orika mapping frameworkがあります。
より高度な制御を可能にする高度な機能は間違いなくありますが、ほとんどのユースケースでは、ここで説明する機能で十分です。
完全なプロジェクトコードとすべての例は、私のgithub projectにあります。 Dozer mapping frameworkについてもチュートリアルを確認することを忘れないでください。どちらも、ほぼ同じ問題を解決します。