Mapping mit Orika

1. Übersicht

Orika ist ein Java Bean-Mapping-Framework, das Daten rekursiv von einem Objekt in ein anderes kopiert . Dies kann bei der Entwicklung mehrschichtiger Anwendungen sehr nützlich sein.

Beim Hin- und Herbewegen von Datenobjekten zwischen diesen Ebenen wird häufig festgestellt, dass Objekte von einer Instanz in eine andere konvertiert werden müssen, um unterschiedliche APIs aufzunehmen.

Einige Möglichkeiten, dies zu erreichen, sind: Festes Codieren der Kopierlogik oder Implementieren von Bean-Mappern wie Dozer . Es kann jedoch verwendet werden, um den Prozess der Zuordnung zwischen einer Objektschicht und einer anderen zu vereinfachen.

Orika verwendet die Bytecode-Generierung, um schnelle Mapper mit minimalem Overhead zu erstellen. Dies macht sie viel schneller als andere reflexionsbasierte Mapper wie Dozer.

2. Einfaches Beispiel

Der grundlegende Eckpfeiler des Mapping-Frameworks ist die MapperFactory- Klasse. Dies ist die Klasse, mit der wir Zuordnungen konfigurieren und die MapperFacade- Instanz abrufen, die die eigentliche Zuordnungsarbeit ausführt.

Wir erstellen ein MapperFactory- Objekt wie folgt :

MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();

Nehmen wir dann an, wir haben ein Quelldatenobjekt, Source.java , mit zwei Feldern:

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 }

Und ein ähnliches Zieldatenobjekt , 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 }

Dies ist die grundlegendste Bohnenzuordnung mit Orika:

@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()); }

Wie wir beobachten können, haben wir einfach durch Zuordnung ein Dest- Objekt mit identischen Feldern wie Source erstellt . Standardmäßig ist auch eine bidirektionale oder umgekehrte Zuordnung möglich:

@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 Setup

Um Orika Mapper in unseren Maven-Projekten verwenden zu können, benötigen wir eine Orika-Core- Abhängigkeit in pom.xml :

 ma.glasnost.orika orika-core 1.4.6 

Die neueste Version finden Sie immer hier.

3. Arbeiten mit MapperFactory

Das allgemeine Muster der Zuordnung mit Orika besteht darin, ein MapperFactory- Objekt zu erstellen , es zu konfigurieren, falls das Standardzuordnungsverhalten angepasst werden muss, ein MapperFacade- Objekt daraus zu erhalten und schließlich die tatsächliche Zuordnung vorzunehmen .

Wir werden dieses Muster in all unseren Beispielen beobachten. Unser erstes Beispiel zeigte jedoch das Standardverhalten des Mappers ohne Änderungen von unserer Seite.

3.1. Die BoundMapperFacade gegen MapperFacade

Eine Sache zu beachten ist, dass wir BoundMapperFacade anstelle der Standard- MapperFacade verwenden können, die ziemlich langsam ist. Dies sind Fälle, in denen wir ein bestimmtes Paar von Typen zuordnen müssen.

Unser erster Test würde also werden:

@Test public void givenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect() { BoundMapperFacade 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()); }

Damit BoundMapperFacade jedoch bidirektional zugeordnet werden kann, müssen wir die mapReverse- Methode explizit aufrufen und nicht die Kartenmethode , die wir für den Fall der Standard- MapperFacade untersucht haben :

@Test public void givenSrcAndDest_whenMapsUsingBoundMapperInReverse_thenCorrect() { BoundMapperFacade 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()); }

Andernfalls schlägt der Test fehl.

3.2. Feldzuordnungen konfigurieren

Die Beispiele, die wir bisher betrachtet haben, betreffen Quell- und Zielklassen mit identischen Feldnamen. Dieser Unterabschnitt befasst sich mit dem Fall, in dem zwischen beiden ein Unterschied besteht.

Stellen Sie sich ein Quellobjekt, Person , mit drei Feldern vor, nämlich Name , Spitzname und Alter :

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 }

Dann hat eine andere Ebene der Anwendung ein ähnliches Objekt, das jedoch von einem französischen Programmierer geschrieben wurde. Nehmen wir an, das heißt Personne , mit den Feldern nom , surnom und age , die alle den obigen drei entsprechen:

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 kann diese Unterschiede nicht automatisch beheben. Wir können jedoch die ClassMapBuilder- API verwenden, um diese eindeutigen Zuordnungen zu registrieren.

Wir haben es bereits zuvor verwendet, aber wir haben noch keine seiner leistungsstarken Funktionen genutzt. Die erste Zeile jedes unserer vorhergehenden Tests mit der Standard- MapperFacade verwendete die ClassMapBuilder- API, um die beiden Klassen zu registrieren, die wir zuordnen wollten:

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

Wir könnten auch alle Felder mit der Standardkonfiguration zuordnen, um es klarer zu machen:

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

Durch Hinzufügen des Methodenaufrufs byDefault () konfigurieren wir das Verhalten des Mappers bereits mithilfe der ClassMapBuilder- API.

Jetzt wollen wir in der Lage sein , zu kartieren Personne zu Person , so dass wir auch configure Feldzuordnungen auf die Mapper mit 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()); }

Don't forget to call the register() API method in order to register the configuration with the MapperFactory.

Even if only one field differs, going down this route means we must explicitly register all field mappings, including age which is the same in both objects, otherwise the unregistered field will not be mapped and the test would fail.

This will soon become tedious, what if we only want to map one field out of 20, do we need to configure all of their mappings?

No, not when we tell the mapper to use it's default mapping configuration in cases where we have not explicitly defined a mapping:

mapperFactory.classMap(Personne.class, Person.class) .field("nom", "name").field("surnom", "nickname").byDefault().register();

Here, we have not defined a mapping for the age field, but nevertheless the test will pass.

3.3. Exclude a Field

Assuming we would like to exclude the nom field of Personne from the mapping – so that the Person object only receives new values for fields that are not excluded:

@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()); }

Notice how we exclude it in the configuration of the MapperFactory and then notice also the first assertion where we expect the value of name in the Person object to remain null, as a result of it being excluded in mapping.

4. Collections Mapping

Sometimes the destination object may have unique attributes while the source object just maintains every property in a collection.

4.1. Lists and Arrays

Consider a source data object that only has one field, a list of a person's names:

public class PersonNameList { private List nameList; public PersonNameList(List nameList) { this.nameList = nameList; } }

Now consider our destination data object which separates firstName and lastName into separate fields:

public class PersonNameParts { private String firstName; private String lastName; public PersonNameParts(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }

Let's assume we are very sure that at index 0 there will always be the firstName of the person and at index 1 there will always be their lastName.

Orika allows us to use the bracket notation to access members of a collection:

@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"); }

Even if instead of PersonNameList, we had PersonNameArray, the same test would pass for an array of names.

4.2. Maps

Assuming our source object has a map of values. We know there is a key in that map, first, whose value represents a person's firstName in our destination object.

Likewise we know that there is another key, last, in the same map whose value represents a person's lastName in the destination object.

public class PersonNameMap { private Map nameMap; public PersonNameMap(Map nameMap) { this.nameMap = nameMap; } }

Similar to the case in the preceding section, we use bracket notation, but instead of passing in an index, we pass in the key whose value we want to map to the given destination field.

Orika accepts two ways of retrieving the key, both are represented in the following test:

@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"); }

We can use either single quotes or double quotes but we must escape the latter.

5. Map Nested Fields

Following on from the preceding collections examples, assume that inside our source data object, there is another Data Transfer Object (DTO) that holds the values we want to map.

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

To be able to access the properties of the nested DTO and map them onto our destination object, we use dot notation, like so:

@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. Mapping Null Values

In some cases, you may wish to control whether nulls are mapped or ignored when they are encountered. By default, Orika will map null values when encountered:

@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()); }

This behavior can be customized at different levels depending on how specific we would like to be.

6.1. Global Configuration

We can configure our mapper to map nulls or ignore them at the global level before creating the global MapperFactory. Remember how we created this object in our very first example? This time we add an extra call during the build process:

MapperFactory mapperFactory = new DefaultMapperFactory.Builder() .mapNulls(false).build();

We can run a test to confirm that indeed, nulls are not getting mapped:

@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"); }

What happens is that, by default, nulls are mapped. This means that even if a field value in the source object is null and the corresponding field's value in the destination object has a meaningful value, it will be overwritten.

In our case, the destination field is not overwritten if its corresponding source field has a null value.

6.2. Local Configuration

Mapping of null values can be controlled on a ClassMapBuilder by using the mapNulls(true|false) or mapNullsInReverse(true|false) for controlling mapping of nulls in the reverse direction.

By setting this value on a ClassMapBuilder instance, all field mappings created on the same ClassMapBuilder, after the value is set, will take on that same value.

Let's illustrate this with an example test:

@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"); }

Notice how we call mapNulls just before registering name field, this will cause all fields following the mapNulls call to be ignored when they have null value.

Bi-directional mapping also accepts mapped null values:

@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()); }

Also we can prevent this by calling mapNullsInReverse and passing in 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. Field Level Configuration

We can configure this at the field level using fieldMap, like so:

mapperFactory.classMap(Source.class, Dest.class).field("age", "age") .fieldMap("name", "name").mapNulls(false).add().byDefault().register();

In this case, the configuration will only affect the name field as we have called it at field level:

@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 Custom Mapping

So far, we have looked at simple custom mapping examples using the ClassMapBuilder API. We shall still use the same API but customize our mapping using Orika's CustomMapper class.

Assuming we have two data objects each with a certain field called dtob, representing the date and time of the birth of a person.

One data object represents this value as a datetime String in the following ISO format:

2007-06-26T21:22:39Z

and the other represents the same as a long type in the following unix timestamp format:

1182882159000

Clearly, non of the customizations we have covered so far suffices to convert between the two formats during the mapping process, not even Orika's built in converter can handle the job. This is where we have to write a CustomMapper to do the required conversion during mapping.

Let us create our first data object:

public class Person3 { private String name; private String dtob; public Person3(String name, String dtob) { this.name = name; this.dtob = dtob; } }

then our second data object:

public class Personne3 { private String name; private long dtob; public Personne3(String name, long dtob) { this.name = name; this.dtob = dtob; } }

We will not label which is source and which is destination right now as the CustomMapper enables us to cater for bi-directional mapping.

Here is our concrete implementation of the CustomMapper abstract class:

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

Notice that we have implemented methods mapAtoB and mapBtoA. Implementing both makes our mapping function bi-directional.

Each method exposes the data objects we are mapping and we take care of copying the field values from one to the other.

There in is where we write the custom code to manipulate the source data according to our requirements before writing it to the destination object.

Let's run a test to confirm that our custom mapper works:

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

Beachten Sie, dass wir den benutzerdefinierten Mapper wie alle anderen einfachen Anpassungen weiterhin über die ClassMapBuilder- API an Orikas Mapper übergeben .

Wir können auch bestätigen, dass bidirektionales Mapping funktioniert:

@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. Fazit

In diesem Artikel haben wir die wichtigsten Funktionen des Orika-Mapping-Frameworks untersucht .

Es gibt definitiv erweiterte Funktionen, die uns viel mehr Kontrolle geben, aber in den meisten Anwendungsfällen sind die hier behandelten mehr als ausreichend.

Den vollständigen Projektcode und alle Beispiele finden Sie in meinem Github-Projekt. Vergessen Sie nicht, auch unser Tutorial zum Dozer-Mapping-Framework zu lesen, da beide mehr oder weniger das gleiche Problem lösen.