Orikaとのマッピング

1概要

Orika は、あるオブジェクトから別のオブジェクトにデータを再帰的にコピーするJava Beanマッピングフレームワークです。多層アプリケーションを開発するときに非常に役立ちます。

これらのレイヤ間でデータオブジェクトをやり取りしながら、異なる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>

最新版は常にhttps://search.maven.org/classic/#search%7Cga%7C1%7Ca%3A%22orika-core%22[here]にあります。

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 を双方向にマップするには、デフォルトの MapperFacade の場合に見たmapメソッドではなく mapReverse メソッドを明示的に呼び出す必要があります。

@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. フィールドマッピングを設定する

これまで見てきた例は、同じフィールド名を持つ送信元クラスと宛先クラスを含みます。このサブセクションでは、両者に違いがある場合に取り組みます。

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 ** を使うことができます

私たちはすでにそれを使ったことがありますが、その強力な機能をまだ利用していません。デフォルトの MapperFacade を使用した上記の各テストの最初の行は、 ClassMapBuilder を使用していました。

mapperFactory.classMap(Source.class, Dest.class);

より明確にするために、デフォルト設定を使用してすべてのフィールドをマッピングすることもできます。

mapperFactory.classMap(Source.class, Dest.class).byDefault()

byDefault() メソッド呼び出しを追加することで、 ClassMapBuilder を使用してマッパーの動作を既に構成しています。

今度は Personne Person にマップできるようにしたいので、 ClassMapBuilder を使用してマッパーへのフィールドマッピングも設定します。

@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のうち一つのフィールドをマッピングしたいだけなら 、それらのマッピングすべてを設定する必要がありますか?

いいえ、マッピングを明示的に定義していない場合にマッパーにデフォルトのマッピング設定を使用するように指示したときではありません。

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<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;
    }
}

前のセクションの場合と同様に、括弧表記を使用しますが、インデックスを渡す代わりに、値を指定されたdestinationフィールドにマッピングするキーを渡します。

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が発生したときに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 値のマッピングは、逆方向の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 fieldを登録する直前に 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. オリカカスタムマッピング

これまでは、 ClassMapBuilder を使用して簡単なカスタムマッピングの例を見てきました。

2つのデータオブジェクトがあり、それぞれが dtob という名前の特定のフィールドを持ち、個人の生年月日を表します。

1つのデータオブジェクトは、この値を次のISO形式の日時文字列__として表します。

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 を介してカスタムマッパーを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マッピングフレームワークの最も重要な機能について説明しました。

より高度な制御を可能にする、より高度な機能が確実にありますが、ほとんどの使用例では、ここで説明した機能で十分です。

完全なプロジェクトコードとすべての例は私のhttps://github.com/eugenp/tutorials/tree/master/orika[githubプロジェクト]にあります。

/dozer[Dozerマッピングフレームワーク]リンクも、ほぼ同じ問題を解決するため、リンク上のチュートリアルをチェックすることを忘れないでください。

前の投稿:curlを使ってREST APIをテストする
次の投稿:Javaで16進数をASCIIに変換