Orikaによるマッピング
1. 概要
Orika は、あるオブジェクトから別のオブジェクトにデータを再帰的にコピーするJavaBeanマッピングフレームワークです。 多層アプリケーションを開発するときに非常に役立ちます。
これらのレイヤー間でデータオブジェクトを前後に移動しているときに、さまざまなAPIに対応するために、オブジェクトをあるインスタンスから別のインスタンスに変換する必要があることがよくあります。
これを実現するいくつかの方法は次のとおりです。コピーロジックをハードコーディングするか、DozerのようなBeanマッパーを実装します。 ただし、あるオブジェクトレイヤーと別のオブジェクトレイヤーの間のマッピングプロセスを簡素化するために使用できます。
Orika は、バイトコード生成を使用して、最小限のオーバーヘッドで高速マッパーを作成し、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("Baeldung", 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("Baeldung", 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依存関係が必要です。
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.4.6</version>
</dependency>
最新バージョンはいつでもここで見つけることができます。
3. MapperFactoryの操作
Orikaを使用したマッピングの一般的なパターンでは、 MapperFactory オブジェクトを作成し、デフォルトのマッピング動作を微調整する必要がある場合に備えて構成し、そこから MapperFacade オブジェクトを取得し、最後に実際のマッピングを行います。
すべての例でこのパターンを観察します。 しかし、最初の例では、マッパーのデフォルトの動作を、私たちの側からの調整なしで示しました。
3.1. BoundMapperFacadeとMapperFacade
注意すべき点の1つは、非常に遅いデフォルトのMapperFacadeよりもBoundMapperFacadeを使用することを選択できることです。 これらは、マップするタイプの特定のペアがある場合です。
したがって、最初のテストは次のようになります。
@Test
public void givenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect() {
BoundMapperFacade<Source, Dest>
boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
Source src = new Source("baeldung", 10);
Dest dest = boundMapper.map(src);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
ただし、 BoundMapperFacade を双方向にマップするには、デフォルトの
@Test
public void givenSrcAndDest_whenMapsUsingBoundMapperInReverse_thenCorrect() {
BoundMapperFacade<Source, Dest>
boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
Dest src = new Dest("baeldung", 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はこれらの違いを自動的に解決することはできません。 ただし、 ClassMapBuilder APIを使用して、これらの一意のマッピングを登録できます。
以前に使用したことがありますが、その強力な機能はまだ利用していません。 デフォルトのMapperFacadeを使用した前述の各テストの最初の行は、 ClassMapBuilder APIを使用して、マップする2つのクラスを登録しました。
mapperFactory.classMap(Source.class, Dest.class);
明確にするために、デフォルト構成を使用してすべてのフィールドをマップすることもできます。
mapperFactory.classMap(Source.class, Dest.class).byDefault()
byDefault()メソッド呼び出しを追加することで、 ClassMapBuilder APIを使用してマッパーの動作を既に構成しています。
ここで、PersonneをPersonにマッピングできるようにしたいので、 ClassMapBuilder API:を使用してマッパーへのフィールドマッピングも構成します。
@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 フィールドマッピングを明示的に登録する必要があります。そうしないと、未登録のフィールドはマッピングされません。テストは失敗します。
これはすぐに面倒になります。20のうち1つのフィールドのみをマップする場合、すべてのマッピングを構成する必要がありますか?
いいえ、マッピングを明示的に定義していない場合に、マッパーにデフォルトのマッピング構成を使用するように指示したときではありません。
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の値がのままであると予想される最初のアサーションにも注目してください。 X200X] null 、マッピングから除外された結果。
4. コレクションのマッピング
ソースオブジェクトがコレクション内のすべてのプロパティを維持しているのに、宛先オブジェクトが一意の属性を持っている場合があります。
4.1. リストと配列
1つのフィールド、つまり人の名前のリストしかないソースデータオブジェクトについて考えてみます。
public class PersonNameList {
private List<String> nameList;
public PersonNameList(List<String> 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<String> 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. マップ
ソースオブジェクトに値のマップがあると仮定します。 そのマップにキーfirstがあり、その値は宛先オブジェクト内の人のfirstNameを表します。
同様に、同じマップに別のキー last があり、その値は宛先オブジェクト内の人のlastNameを表していることがわかります。
public class PersonNameMap {
private Map<String, String> nameMap;
public PersonNameMap(Map<String, String> 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<String, String> 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が検出されたときに、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 であり、宛先オブジェクトの対応するフィールドの値に意味のある値がある場合でも、上書きされることを意味します。
この場合、対応するソースフィールドの値が 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を呼び出す方法に注意してください。これにより、 mapNulls 呼び出しに続くすべてのフィールドが、nullがある場合に無視されます。 ] 価値。
双方向マッピングは、マップされた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カスタムマッピング
これまで、を使用した簡単なカスタムマッピングの例を見てきました。 ClassMapBuilder
それぞれがdtobと呼ばれる特定のフィールドを持つ、人の誕生の日付と時刻を表す2つのデータオブジェクトがあると仮定します。
1つのデータオブジェクトは、この値を次のISO形式の datetimeStringとして表します。
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<Personne3, Person3> {
@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を実装していることに注意してください。 両方を実装すると、マッピング関数が双方向になります。
各メソッドは、マッピングしているデータオブジェクトを公開し、フィールド値を一方から他方にコピーします。
ここで、宛先オブジェクトに書き込む前に、要件に従ってソースデータを操作するカスタムコードを記述します。
テストを実行して、カスタムマッパーが機能することを確認しましょう。
@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);
}
他のすべての単純なカスタマイズと同様に、 ClassMapBuilder APIを介してカスタムマッパーを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. 結論
この記事では、Orikaマッピングフレームワークの最も重要な機能について説明しました。
確かに、はるかに詳細な制御を可能にするより高度な機能がありますが、ほとんどのユースケースでは、ここで説明する機能で十分です。
完全なプロジェクトコードとすべての例は、私のgithubプロジェクトにあります。 ドーザーマッピングフレームワークのチュートリアルも忘れずにチェックしてください。どちらもほぼ同じ問題を解決します。