Pemetaan dengan Orika

1. Ikhtisar

Orika adalah kerangka kerja pemetaan Java Bean yang secara rekursif menyalin data dari satu objek ke objek lainnya . Ini bisa sangat berguna saat mengembangkan aplikasi berlapis-lapis.

Saat memindahkan objek data bolak-balik di antara lapisan-lapisan ini, adalah umum untuk mengetahui bahwa kita perlu mengonversi objek dari satu instance ke instance lainnya untuk mengakomodasi API yang berbeda.

Beberapa cara untuk mencapai ini adalah: hard coding logika penyalinan atau untuk menerapkan pembuat peta kacang seperti Dozer . Namun, ini dapat digunakan untuk menyederhanakan proses pemetaan antara satu lapisan objek dan lainnya.

Orika menggunakan pembuatan kode byte untuk membuat pembuat peta cepat dengan overhead minimal, membuatnya jauh lebih cepat daripada pembuat peta berbasis refleksi lainnya seperti Dozer.

2. Contoh Sederhana

Landasan dasar dari kerangka kerja pemetaan adalah kelas MapperFactory . Ini adalah kelas yang akan kita gunakan untuk mengonfigurasi pemetaan dan mendapatkan instance MapperFacade yang melakukan pekerjaan pemetaan sebenarnya.

Kami membuat objek MapperFactory seperti ini:

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

Kemudian asumsikan kita memiliki 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 tujuan 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 dasar 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 identik sebagai Sumber , hanya dengan memetakan. Pemetaan dua arah atau terbalik juga dimungkinkan secara default:

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

Untuk menggunakan mapper Orika dalam proyek maven kita, kita perlu memiliki ketergantungan orika-core di pom.xml :

 ma.glasnost.orika orika-core 1.4.6 

Versi terbaru selalu dapat ditemukan di sini.

3. Bekerja Dengan MapperFactory

Pola umum pemetaan dengan Orika melibatkan pembuatan objek MapperFactory , mengonfigurasinya jika kita perlu mengubah perilaku pemetaan default, mendapatkan objek MapperFacade darinya dan akhirnya, pemetaan aktual.

Kita akan mengamati pola ini dalam semua contoh kita. Tetapi contoh pertama kami menunjukkan perilaku default mapper tanpa perubahan apa pun dari pihak kami.

3.1. The BoundMapperFacade vs MapperFacade

Satu hal yang perlu diperhatikan adalah kita dapat memilih untuk menggunakan BoundMapperFacade daripada MapperFacade default yang cukup lambat. Ini adalah kasus di mana kami memiliki pasangan tipe tertentu untuk dipetakan.

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

Namun, agar BoundMapperFacade dapat memetakan dua arah, kita harus secara eksplisit memanggil metode mapReverse daripada metode peta yang telah kita lihat untuk kasus MapperFacade default :

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

Tes akan gagal jika tidak.

3.2. Konfigurasikan Pemetaan Bidang

Contoh yang telah kita lihat sejauh ini melibatkan kelas sumber dan tujuan dengan nama bidang yang identik. Subbagian ini menangani kasus di mana ada perbedaan di antara keduanya.

Pertimbangkan objek sumber, Orang , dengan tiga bidang yaitu nama , nama panggilan , dan usia :

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 lain dari aplikasi memiliki objek serupa, tetapi ditulis oleh programmer perancis. Katakanlah itu disebut Personne , dengan nom bidang , marga dan usia , semuanya sesuai dengan tiga 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 secara otomatis menyelesaikan perbedaan ini. Tapi kita bisa menggunakan API ClassMapBuilder untuk mendaftarkan pemetaan unik ini.

Kami telah menggunakannya sebelumnya, tetapi kami belum memanfaatkan fitur-fiturnya yang canggih. Baris pertama dari setiap pengujian sebelumnya yang menggunakan MapperFacade default 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 default, untuk membuatnya lebih jelas:

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

Dengan menambahkan panggilan metode byDefault () , kita sudah mengonfigurasi perilaku mapper menggunakan API ClassMapBuilder .

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

Perhatikan bahwa kita masih meneruskan mapper kustom ke mapper Orika melalui API ClassMapBuilder , sama seperti semua kustomisasi sederhana lainnya.

Kami juga dapat memastikan bahwa 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. Kesimpulan

Pada artikel ini, kami telah menjelajahi fitur-fitur terpenting dari kerangka kerja pemetaan Orika .

Jelas ada fitur yang lebih canggih yang memberi kita lebih banyak kendali tetapi dalam kebanyakan kasus penggunaan, yang dibahas di sini sudah lebih dari cukup.

Kode proyek lengkap dan semua contoh dapat ditemukan di proyek github saya. Jangan lupa untuk melihat tutorial kami tentang kerangka kerja pemetaan Dozer juga, karena keduanya menyelesaikan masalah yang kurang lebih sama.