Pemetaan dengan Orika

1. Gambaran keseluruhan

Orika adalah kerangka pemetaan Java Bean yang secara berulang-ulang menyalin data dari satu objek ke objek lain . Ia sangat berguna ketika mengembangkan aplikasi berlapis-lapis.

Semasa memindahkan objek data bolak-balik di antara lapisan ini, adalah umum untuk menemukan bahawa kita perlu mengubah objek dari satu contoh ke yang lain untuk mengakomodasi API yang berbeza.

Beberapa cara untuk mencapainya adalah: pengekodan logik penyalinan keras atau pelaksanaan pemetaan kacang seperti Dozer . Namun, ia dapat digunakan untuk mempermudah proses pemetaan antara satu lapisan objek dengan lapisan yang lain.

Orika menggunakan penjanaan kod bait untuk membuat pemetaan pantas dengan overhead minimum, menjadikannya lebih cepat daripada pemetaan berasaskan refleksi lain seperti Dozer.

2. Contoh Ringkas

Landasan asas kerangka pemetaan adalah kelas MapperFactory . Ini adalah kelas yang akan kami gunakan untuk mengkonfigurasi pemetaan dan mendapatkan contoh MapperFacade yang melakukan kerja pemetaan sebenarnya.

Kami membuat objek MapperFactory seperti:

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

Kemudian dengan anggapan kita mempunyai objek data sumber, Source.java , dengan dua bidang:

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 }

Dan objek data destinasi yang serupa, 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 }

Ini adalah pemetaan kacang paling asas menggunakan 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()); }

Seperti yang dapat kita amati, kita telah membuat objek Dest dengan bidang yang sama dengan Source , hanya dengan pemetaan. Pemetaan dua arah atau terbalik juga mungkin dilakukan secara lalai:

@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. Persediaan Maven

Untuk menggunakan Orika mapper dalam projek maven kami, kita perlu mempunyai pergantungan inti orika dalam pom.xml :

 ma.glasnost.orika orika-core 1.4.6 

Versi terbaru boleh didapati di sini.

3. Bekerja Dengan MapperFactory

Pola pemetaan umum dengan Orika melibatkan membuat objek MapperFactory , mengkonfigurasinya sekiranya kita perlu mengubah tingkah laku pemetaan lalai, mendapatkan objek MapperFacade darinya dan akhirnya, pemetaan sebenarnya.

Kita akan memerhatikan corak ini dalam semua contoh kita. Tetapi contoh pertama kami menunjukkan tingkah laku lalai pemeta tanpa ada perubahan dari pihak kami.

3.1. The BoundMapperFacade vs MapperFacade

Satu perkara yang perlu diperhatikan ialah kita dapat memilih untuk menggunakan BoundMapperFacade berbanding MapperFacade lalai yang agak perlahan. Ini adalah kes di mana kita mempunyai sepasang jenis tertentu untuk dipetakan.

Oleh itu, ujian awal kami akan menjadi:

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

Walau bagaimanapun, untuk peta BoundMapperFacade dua arah, kita harus memanggil kaedah mapReverse secara jelas dan bukannya kaedah peta yang telah kita lihat untuk kes MapperFacade lalai :

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

Ujian akan gagal sebaliknya.

3.2. Konfigurasikan Pemetaan Medan

Contoh yang telah kita lihat setakat ini melibatkan kelas sumber dan destinasi dengan nama bidang yang sama. Subseksyen ini menangani kes di mana terdapat perbezaan antara keduanya.

Pertimbangkan objek sumber, Orang , dengan tiga bidang iaitu nama , nama panggilan dan umur :

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 }

Kemudian lapisan aplikasi yang lain mempunyai objek yang serupa, tetapi ditulis oleh pengaturcara Perancis. Katakan itu dipanggil Personne , dengan bidang nom , nama keluarga dan umur , semuanya sesuai dengan tiga perkara di atas:

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 tidak dapat menyelesaikan perbezaan ini secara automatik. Tetapi kita boleh menggunakan ClassMapBuilder API untuk mendaftarkan pemetaan unik ini.

Kami telah menggunakannya sebelum ini, tetapi kami belum menggunakan ciri-ciri hebatnya. Baris pertama dari setiap ujian kami yang terdahulu menggunakan MapperFacade lalai menggunakan API ClassMapBuilder untuk mendaftarkan dua kelas yang ingin kami petakan :

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

Kami juga dapat memetakan semua bidang menggunakan konfigurasi lalai, untuk membuatnya lebih jelas:

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

Dengan menambahkan panggilan kaedah byDefault () , kita sudah mengkonfigurasi tingkah laku mapper menggunakan ClassMapBuilder API.

Sekarang kami ingin dapat memetakan Personne to Person , jadi kami juga mengkonfigurasi pemetaan bidang ke mapper menggunakan 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); }

Perhatikan bahawa kita masih meneruskan mapper khusus ke mapper Orika melalui ClassMapBuilder API, seperti semua penyesuaian mudah yang lain.

Kami juga dapat mengesahkan bahawa pemetaan dua arah berfungsi:

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

Dalam artikel ini, kami telah meneroka ciri-ciri terpenting dalam rangka pemetaan Orika .

Pasti ada ciri yang lebih maju yang memberi kita kawalan lebih banyak tetapi dalam kebanyakan kes penggunaan, yang diliputi di sini akan lebih daripada cukup.

Kod projek lengkap dan semua contoh boleh didapati di projek github saya. Jangan lupa untuk melihat tutorial kami mengenai kerangka pemetaan Dozer juga, kerana kedua-duanya menyelesaikan lebih kurang masalah yang sama.