Mappatura con Orika

1. Panoramica

Orika è un framework di mappatura Java Bean che copia ricorsivamente i dati da un oggetto a un altro . Può essere molto utile quando si sviluppano applicazioni a più livelli.

Durante lo spostamento di oggetti dati avanti e indietro tra questi livelli, è comune scoprire che è necessario convertire gli oggetti da un'istanza all'altra per ospitare API diverse.

Alcuni modi per ottenere ciò sono: codificare hard la logica di copia o implementare i bean mapper come Dozer . Tuttavia, può essere utilizzato per semplificare il processo di mappatura tra un livello oggetto e un altro.

Orika utilizza la generazione di byte code per creare mappatori veloci con un sovraccarico minimo, rendendolo molto più veloce di altri mappatori basati sulla riflessione come Dozer.

2. Semplice esempio

La pietra angolare di base del framework di mappatura è la classe MapperFactory . Questa è la classe che useremo per configurare i mapping e ottenere l' istanza MapperFacade che esegue il lavoro di mapping effettivo.

Creiamo un oggetto MapperFactory in questo modo:

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

Quindi supponendo di avere un oggetto dati di origine, Source.java , con due campi:

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 }

E un oggetto dati di destinazione simile, 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 }

Questa è la più semplice mappatura dei bean utilizzando 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()); }

Come possiamo osservare, abbiamo creato un oggetto Dest con campi identici a Source , semplicemente mappando. Per impostazione predefinita è possibile anche la mappatura bidirezionale o inversa:

@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. Installazione di Maven

Per utilizzare Orika mapper nei nostri progetti maven, dobbiamo avere una dipendenza orika-core in pom.xml :

 ma.glasnost.orika orika-core 1.4.6 

L'ultima versione è sempre disponibile qui.

3. Lavorare con MapperFactory

Il modello generale di mappatura con Orika implica la creazione di un oggetto MapperFactory , configurandolo nel caso in cui sia necessario modificare il comportamento di mappatura predefinito, ottenere un oggetto MapperFacade da esso e, infine, la mappatura effettiva.

Osserveremo questo modello in tutti i nostri esempi. Ma il nostro primo esempio ha mostrato il comportamento predefinito del mappatore senza alcuna modifica da parte nostra.

3.1. Il BoundMapperFacade vs MapperFacade

Una cosa da notare è che potremmo scegliere di utilizzare BoundMapperFacade sul MapperFacade predefinito che è piuttosto lento. Questi sono casi in cui abbiamo una coppia specifica di tipi da mappare.

Il nostro test iniziale diventerebbe così:

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

Tuttavia, affinché BoundMapperFacade possa mappare in modo bidirezionale, dobbiamo chiamare esplicitamente il metodo mapReverse piuttosto che il metodo map che abbiamo esaminato per il caso del MapperFacade predefinito :

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

Altrimenti il ​​test fallirà.

3.2. Configurare le mappature dei campi

Gli esempi che abbiamo visto finora riguardano classi di origine e destinazione con nomi di campo identici. Questa sottosezione affronta il caso in cui c'è una differenza tra i due.

Considera un oggetto sorgente, Persona , con tre campi: nome , nickname ed età :

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 }

Quindi un altro livello dell'applicazione ha un oggetto simile, ma scritto da un programmatore francese. Diciamo che si chiama Personne , con i campi nom , surnom ed age , tutti corrispondenti ai tre precedenti:

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 non può risolvere automaticamente queste differenze. Ma possiamo usare l' API ClassMapBuilder per registrare queste mappature univoche.

Lo abbiamo già utilizzato in precedenza, ma non abbiamo ancora sfruttato nessuna delle sue potenti funzionalità. La prima riga di ciascuno dei nostri test precedenti utilizzando il MapperFacade predefinito utilizzava l' API ClassMapBuilder per registrare le due classi che volevamo mappare:

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

Potremmo anche mappare tutti i campi utilizzando la configurazione predefinita, per renderlo più chiaro:

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

Aggiungendo la chiamata al metodo byDefault () , stiamo già configurando il comportamento del mapper utilizzando l' API ClassMapBuilder .

Ora vogliamo essere in grado di mappare Personne su Person , quindi configuriamo anche i mapping dei campi sul mappatore utilizzando l' API 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()); }

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

Si noti che passiamo ancora il mappatore personalizzato al mappatore di Orika tramite l' API ClassMapBuilder , proprio come tutte le altre semplici personalizzazioni.

Possiamo anche confermare che la mappatura bidirezionale funziona:

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

In questo articolo, abbiamo esplorato le caratteristiche più importanti del framework di mappatura Orika .

Ci sono sicuramente funzionalità più avanzate che ci danno molto più controllo, ma nella maggior parte dei casi d'uso, quelle trattate qui saranno più che sufficienti.

Il codice completo del progetto e tutti gli esempi possono essere trovati nel mio progetto GitHub. Non dimenticare di controllare anche il nostro tutorial sul framework di mappatura Dozer, poiché entrambi risolvono più o meno lo stesso problema.