Mapiranje s Orikom

1. Pregled

Orika je Java Mapov okvir za mapiranje koji rekurzivno kopira podatke s jednog objekta na drugi. To može biti vrlo korisno pri razvoju višeslojnih aplikacija.

Dok premještamo podatkovne objekte naprijed-natrag između ovih slojeva, uobičajeno je ustanoviti da moramo pretvoriti objekte iz jedne instance u drugu kako bismo smjestili različite API-je.

Neki od načina da se to postigne su: teško kodiranje logike kopiranja ili za implementaciju mapa graha poput Dozera. Međutim, može se koristiti za pojednostavljivanje postupka mapiranja između jednog i drugog sloja objekta.

Orika koristi generiranje bajt koda za stvaranje brzih mapera s minimalnim troškovima, što ga čini mnogo bržim od ostalih mapa zasnovanih na refleksiji poput Dozera.

2. Jednostavan primjer

Temeljni kamen okvira za mapiranje je MapperFactory razred. To je klasa koju ćemo koristiti za konfiguriranje preslikavanja i dobivanje MapperFacade instance koja izvodi stvarni posao mapiranja.

Mi stvaramo MapperFactory objekt poput ovog:

MapperFactory mapperFactory = novo DefaultMapperFactory.Builder (). Build ();

Tada pod pretpostavkom da imamo izvorni objekt podataka, Izvor.java, s dva polja:

izvor javne klase {naziv privatnog niza; privatno int doba; javni izvor (ime niza, int age) {this.name = name; this.age = dob; } // standardni getteri i postavljači}

I sličan objekt podataka odredišta, Dest.java:

javna klasa Dest {naziv privatnog niza; privatno int doba; javno Dest (Ime niza, int age) {this.name = name; this.age = dob; } // standardni getteri i postavljači}

Ovo je najosnovnije mapiranje graha pomoću Orike:

@Test javna praznina givenSrcAndDest_whenMaps_thenCorrect () {mapperFactory.classMap (Source.class, Dest.class); MapperFacade mapper = mapperFactory.getMapperFacade (); Izvor src = novi Izvor ("Baeldung", 10); Dest dest = mapper.map (src, Dest.class); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Kao što možemo primijetiti, stvorili smo Dest objekt s identičnim poljima kao Izvor, jednostavno mapiranjem. Dvosmjerno ili obrnuto mapiranje također je prema zadanim postavkama moguće:

@Test javna praznina givenSrcAndDest_whenMapsReverse_thenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .byDefault (); MapperFacade mapper = mapperFactory.getMapperFacade (); Dest src = novi Dest ("Baeldung", 10); Izvor dest = mapper.map (src, Source.class); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

3. Postavljanje Mavena

Da bismo koristili Orika mapper u našim maven projektima, to moramo imati orika-jezgra ovisnost u pom.xml:

 ma.glasnost.orika orika-core 1.4.6 

Najnoviju verziju uvijek možete pronaći ovdje.

3. Rad s MapperFactory

Opći obrazac mapiranja s Orikom uključuje stvaranje a MapperFactory objekt, konfigurirajući ga u slučaju da trebamo podesiti zadano ponašanje mapiranja, dobivanjem a MapperFacade objekt iz njega i konačno, stvarno mapiranje.

Taj ćemo obrazac promatrati u svim našim primjerima. No, naš prvi primjer pokazao je zadano ponašanje mappera bez ikakvog podešavanja s naše strane.

3.1. The BoundMapperFacade nasuprot MapperFacade

Treba napomenuti da bismo mogli odabrati upotrebu BoundMapperFacade preko zadanog MapperFacade što je prilično sporo. To su slučajevi u kojima imamo određeni par tipova za mapiranje.

Naš bi početni test tako postao:

@Test javna praznina givenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect () {BoundMapperFacade boundMapper = mapperFactory.getMapperFacade (Source.class, Dest.class); Izvor src = novi Izvor ("baeldung", 10); Dest dest = boundMapper.map (src); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Međutim, za BoundMapperFacade za dvosmjerno mapiranje moramo eksplicitno nazvati mapReverse metoda, a ne metoda mape koju smo gledali za slučaj zadane MapperFacade:

@Test javna praznina givenSrcAndDest_whenMapsUsingBoundMapperInReverse_thenCorrect () {BoundMapperFacade boundMapper = mapperFactory.getMapperFacade (Source.class, Dest.class); Dest src = novi Dest ("baeldung", 10); Izvor dest = boundMapper.mapReverse (src); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Test u protivnom neće uspjeti.

3.2. Konfigurirajte mapiranje polja

Primjeri koje smo do sada gledali uključuju izvorne i odredišne ​​klase s identičnim imenima polja. Ovaj se pododjeljak bavi slučajem kada postoji razlika između njih dvoje.

Razmotrimo izvorni objekt, Osoba , s tri polja naime Ime, nadimak i dob:

javni razred Osoba {ime privatnog niza; private String nadimak; privatno int doba; javna osoba (ime niza, nadimak niza, int age) {this.name = name; this.nickname = nadimak; this.age = dob; } // standardni getteri i postavljači}

Tada još jedan sloj aplikacije ima sličan objekt, ali koji je napisao francuski programer. Recimo da se to zove Personne, s poljima ne m, surnom i dob, sve što odgovara gornjoj trojici:

javni razred Personne {private String nom; privatni gudački surnom; privatno int doba; public Personne (String nom, String surnom, int age) {this.nom = nom; this.surnom = surnom; this.age = dob; } // standardni getteri i postavljači}

Orika ne može automatski riješiti te razlike. Ali možemo koristiti ClassMapBuilder API za registraciju ovih jedinstvenih preslikavanja.

Već smo ga koristili, ali još nismo iskoristili nijednu njegovu moćnu značajku. Prvi redak svakog od naših prethodnih testova koristeći zadani MapperFacade je koristio ClassMapBuilder API za registraciju dvije klase koje smo željeli mapirati:

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

Također bismo mogli mapirati sva polja koristeći zadanu konfiguraciju, kako bismo to učinili jasnijim:

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

Dodavanjem byDefault () poziva metode, već konfiguriramo ponašanje mappera pomoću ClassMapBuilder API.

Sada želimo imati mogućnost mapiranja Personne do Osoba, pa također konfiguriramo mapiranje polja na mapper pomoću ClassMapBuilder API:

@Test javna praznina givenSrcAndDestWithDifferentFieldNames_whenMaps_thenCorrect () {mapperFactory.classMap (Personne.class, Person.class) .field ("nom", "name"). Field ("surnom", "nickname"). Field ("age", " dob "). register (); MapperFacade mapper = mapperFactory.getMapperFacade (); Personne frenchPerson = nova Personne ("Claire", "cla", 25); Osoba englishPerson = mapper.map (frenchPerson, Person.class); assertEquals (englishPerson.getName (), frenchPerson.getNom ()); assertEquals (englishPerson.getNickname (), frenchPerson.getSurnom ()); assertEquals (englishPerson.getAge (), frenchPerson.getAge ()); }

Ne zaboravite nazvati Registar() API metoda kako bi se konfiguracija registrirala s MapperFactory.

Čak i ako se razlikuje samo jedno polje, spuštanje ovom rutom znači da se moramo izričito registrirati svi mapiranja polja, uključujući dob što je isto u oba objekta, inače neregistrirano polje neće biti mapirano i test neće uspjeti.

Ovo će uskoro postati zamorno, što ako želimo mapirati samo jedno polje od 20, trebamo li konfigurirati sva njihova preslikavanja?

Ne, ne kada kažemo mapperu da koristi zadanu konfiguraciju mapiranja u slučajevima kada nismo izričito definirali mapiranje:

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

Ovdje nismo definirali mapiranje za dob polje, ali bez obzira na to test će proći.

3.3. Izuzmite polje

Pod pretpostavkom da bismo željeli isključiti ne m polje od Personne iz mapiranja - tako da Osoba objekt prima samo nove vrijednosti za polja koja nisu izuzeta:

@Test javna praznina givenSrcAndDest_whenCanExcludeField_thenCorrect () {mapperFactory.classMap (Personne.class, Person.class) .exclude ("nom") .field ("surnom", "nickname"). Field ("age", "age"). Registar(); MapperFacade mapper = mapperFactory.getMapperFacade (); Personne frenchPerson = nova Personne ("Claire", "cla", 25); Osoba englishPerson = mapper.map (frenchPerson, Person.class); assertEquals (null, englishPerson.getName ()); assertEquals (englishPerson.getNickname (), frenchPerson.getSurnom ()); assertEquals (englishPerson.getAge (), frenchPerson.getAge ()); }

Primijetite kako ga isključujemo u konfiguraciji MapperFactory a zatim primijetiti i prvu tvrdnju gdje očekujemo vrijednost Ime u Osoba prigovor da ostane null, što je rezultat njegovog isključivanja u mapiranje.

4. Mapiranje zbirki

Ponekad odredišni objekt može imati jedinstvene atribute, dok izvorni objekt samo održava svako svojstvo u zbirci.

4.1. Popisi i nizovi

Razmotrimo izvorni objekt podataka koji ima samo jedno polje, popis imena osobe:

javna klasa PersonNameList {privatni popis nameList; javni PersonNameList (Popis nameList) {this.nameList = nameList; }}

Sada razmotrite naš odredišni objekt podataka koji se odvaja ime i prezime u zasebna polja:

javna klasa PersonNameParts {private String firstName; private String lastName; javni PersonNameParts (String firstName, String lastName) {this.firstName = firstName; this.lastName = lastName; }}

Pretpostavimo da smo vrlo sigurni da će kod indeksa 0 uvijek postojati ime osobe i kod indeksa 1 uvijek će biti njihova prezime.

Orika nam omogućuje upotrebu nosača zagrada za pristup članovima zbirke:

@Test javna praznina givenSrcWithListAndDestWithPrimitiveAttributes_whenMaps_thenCorrect () {mapperFactory.classMap (PersonNameList.class, PersonNameParts.class) .field ("nameList [0]", "firstName") .field ("nameList." (); MapperFacade mapper = mapperFactory.getMapperFacade (); Popis nameList = Arrays.asList (novi String [] {"Sylvester", "Stallone"}); PersonNameList src = novi PersonNameList (nameList); PersonNameParts dest = mapper.map (src, PersonNameParts.class); assertEquals (dest.getFirstName (), "Sylvester"); assertEquals (dest.getLastName (), "Stallone"); }

Čak i ako umjesto PersonNameList, imali smo PersonNameArray, isti test bi prošao za niz imena.

4.2. Karte

Pod pretpostavkom da naš izvorni objekt ima mapu vrijednosti. Znamo da na toj karti postoji ključ, prvi, čija vrijednost predstavlja vrijednost osobe ime u našem odredišnom objektu.

Isto tako znamo da postoji još jedan ključ, posljednji, na istoj karti čija vrijednost predstavlja vrijednost osobe prezime u odredišnom objektu.

javna klasa PersonNameMap {private map nameMap; javna PersonNameMap (MapMap) {this.nameMap = nameMap; }}

Slično slučaju u prethodnom odjeljku, koristimo notaciju zagrada, ali umjesto da proslijedimo indeks, ključ čiju vrijednost želimo preslikati u dano odredišno polje.

Orika prihvaća dva načina dohvaćanja ključa, oba su predstavljena u sljedećem testu:

@Test javna praznina givenSrcWithMapAndDestWithPrimitiveAttributes_whenMaps_thenCorrect () {mapperFactory.classMap (PersonNameMap.class, PersonNameParts.class) .field ("nameMap ['first']", "firstName"). "lastName") .register (); MapperFacade mapper = mapperFactory.getMapperFacade (); MapMap = nova HashMap (); nameMap.put ("prvi", "Leornado"); nameMap.put ("last", "DiCaprio"); PersonNameMap src = nova PersonNameMap (nameMap); PersonNameParts dest = mapper.map (src, PersonNameParts.class); assertEquals (dest.getFirstName (), "Leornado"); assertEquals (dest.getLastName (), "DiCaprio"); }

Možemo koristiti pojedinačne ili dvostruke navodnike, ali potonjem moramo pobjeći.

5. Mapirajte ugniježđena polja

Slijedeći prethodne primjere zbirki, pretpostavimo da se unutar našeg izvornog objekta podataka nalazi još jedan objekt prijenosa podataka (DTO) koji sadrži vrijednosti koje želimo mapirati.

javna klasa PersonContainer {ime privatnog imena; javni PersonContainer (Ime imena) {this.name = name; }}
naziv javne klase {private String firstName; private String lastName; javno ime (String firstName, String lastName) {this.firstName = firstName; this.lastName = lastName; }}

Da bismo mogli pristupiti svojstvima ugniježđenog DTO-a i mapirati ih na naš odredišni objekt, koristimo točku oznaku, poput ove:

@Test javna praznina givenSrcWithNestedFields_whenMaps_thenCorrect () {mapperFactory.classMap (PersonContainer.class, PersonNameParts.class) .field ("name.firstName", "firstName"). Field ("name.lastName", "lastName"). ; MapperFacade mapper = mapperFactory.getMapperFacade (); PersonContainer src = novi PersonContainer (novo ime ("Nick", "Canon")); PersonNameParts dest = mapper.map (src, PersonNameParts.class); assertEquals (dest.getFirstName (), "Nick"); assertEquals (dest.getLastName (), "Canon"); }

6. Mapiranje null vrijednosti

U nekim ćete slučajevima možda htjeti kontrolirati hoće li se nule mapirati ili ignorirati kada se naiđu. Prema zadanim postavkama, Orika će mapirati null vrijednosti kada se naiđe:

@Test javna praznina givenSrcWithNullField_whenMapsThenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .byDefault (); MapperFacade mapper = mapperFactory.getMapperFacade (); Izvor src = novi izvor (null, 10); Dest dest = mapper.map (src, Dest.class); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Ovo se ponašanje može prilagoditi na različitim razinama, ovisno o tome koliko specifični želimo biti.

6.1. Globalna konfiguracija

Možemo konfigurirati naš mapper da mapira nule ili ih ignorira na globalnoj razini prije stvaranja globalne MapperFactory. Sjećate se kako smo stvorili ovaj objekt u našem prvom primjeru? Ovaj put dodajemo dodatni poziv tijekom postupka izrade:

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

Možemo pokrenuti test kako bismo potvrdili da se nule doista ne mapiraju:

@Test javna praznina givenSrcWithNullAndGlobalConfigForNoNull_whenFailsToMap_ThenCorrect () {mapperFactory.classMap (Source.class, Dest.class); MapperFacade mapper = mapperFactory.getMapperFacade (); Izvor src = novi izvor (null, 10); Dest dest = novi Dest ("Clinton", 55); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), "Clinton"); }

Ono što se događa je da se, prema zadanim postavkama, mapiraju nule. To znači da čak i ako je vrijednost polja u izvornom objektu null a vrijednost odgovarajućeg polja u odredišnom objektu ima značajnu vrijednost, bit će prepisana.

U našem slučaju, odredišno polje nije prebrisano ako njegovo odgovarajuće izvorno polje ima null vrijednost.

6.2. Lokalna konfiguracija

Mapiranje null vrijednosti se mogu kontrolirati na a ClassMapBuilder pomoću mapNulls (true | false) ili mapNullsInReverse (true | false) za kontrolu mapiranja nula u obrnutom smjeru.

Postavljanjem ove vrijednosti na a ClassMapBuilder primjerice, sva mapiranja polja kreirana na istom ClassMapBuilder, nakon što je vrijednost postavljena, poprimit će tu istu vrijednost.

Pokažimo to na primjeru testa:

@Test javna praznina givenSrcWithNullAndLocalConfigForNoNull_whenFailsToMap_ThenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .field ("age", "age") .mapNulls (false) .field ("name", default). ).Registar(); MapperFacade mapper = mapperFactory.getMapperFacade (); Izvor src = novi izvor (null, 10); Dest dest = novi Dest ("Clinton", 55); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), "Clinton"); }

Primijetite kako zovemo mapNulls neposredno prije registracije Ime polje, to će uzrokovati sva polja koja slijede mapNulls poziv da se ignoriraju kad imaju null vrijednost.

Dvosmjerno mapiranje također prihvaća preslikane null vrijednosti:

@Test javna praznina givenDestWithNullReverseMappedToSource_whenMapsByDefault_thenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .byDefault (); MapperFacade mapper = mapperFactory.getMapperFacade (); Dest src = novi Dest (null, 10); Izvor dest = novi Izvor ("Vin", 44); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

To također možemo spriječiti pozivom mapNullsInReverse i prolazeći unutra lažno:

@Test javna praznina givenDestWithNullReverseMappedToSourceAndLocalConfigForNoNull_whenFailsToMap_thenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .field ("age", "age"). (Map). ) .Registar(); MapperFacade mapper = mapperFactory.getMapperFacade (); Dest src = novi Dest (null, 10); Izvor dest = novi Izvor ("Vin", 44); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), "Vin"); }

6.3. Konfiguracija razine polja

To možemo konfigurirati na razini polja pomoću karta polja, ovako:

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

U ovom će slučaju konfiguracija utjecati samo na Ime polje kako smo ga nazvali na razini polja:

@Test javna praznina givenSrcWithNullAndFieldLevelConfigForNoNull_whenFailsToMap_ThenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .field ("age", "age") .fieldMap ("nameN, "lls"). ) .byDefault (). register (); MapperFacade mapper = mapperFactory.getMapperFacade (); Izvor src = novi izvor (null, 10); Dest dest = novi Dest ("Clinton", 55); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), "Clinton"); }

7. Orika prilagođeno mapiranje

Do sada smo pregledali jednostavne primjere prilagođenog mapiranja pomoću ClassMapBuilder API. I dalje ćemo koristiti isti API, ali prilagoditi svoje mapiranje koristeći Orika CustomMapper razred.

Pod pretpostavkom da imamo dva podatkovna objekta od kojih se svaki zove određeno polje dtob, koji predstavlja datum i vrijeme rođenja osobe.

Jedan objekt podataka predstavlja ovu vrijednost kao datetime Niz u sljedećem ISO formatu:

2007-06-26T21: 22: 39Z

a drugi predstavlja isto što i dugo upišite u sljedećem formatu vremenske oznake unix:

1182882159000

Jasno je da nijedna prilagodba koju smo do sada obradili nije dovoljna za pretvorbu između dva formata tijekom postupka mapiranja, čak ni Orikin ugrađeni pretvarač ne može podnijeti posao. Ovdje moramo napisati a CustomMapper da izvrši potrebnu konverziju tijekom mapiranja.

Stvorimo svoj prvi podatkovni objekt:

javna klasa Person3 {ime privatnog niza; privatni niz dtob; javna Person3 (ime niza, niz dtob) {this.name = name; this.dtob = dtob; }}

onda naš drugi podatkovni objekt:

javna klasa Personne3 {ime privatnog niza; privatni dugi dtob; javno Personne3 (Ime niza, dugi dtob) {this.name = name; this.dtob = dtob; }}

Trenutno nećemo označiti koji je izvor, a koje odredište kao CustomMapper omogućuje nam razonodu za dvosmjerno mapiranje.

Evo naše konkretne provedbe CustomMapper sažetak klase:

klasa PersonCustomMapper proširuje CustomMapper {@Override public void mapAtoB (Personne3 a, Person3 b, MappingContext context) {Datum datum = novi datum (a.getDtob ()); DateFormat format = novi SimpleDateFormat ("gggg-MM-dd'T'HH: mm: ss'Z '"); Niz isoDate = format.format (datum); 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 '"); Datum datum = format.parse (b.getDtob ()); duga vremenska oznaka = date.getTime (); a.setDtob (vremenska oznaka); }};

Primijetite da smo primijenili metode mapAtoB i mapBtoA. Implementacija oba čini našu funkciju mapiranja dvosmjernom.

Svaka metoda izlaže podatkovne objekte koje preslikavamo i mi se brinemo za kopiranje vrijednosti polja iz jedne u drugu.

Tu napišemo prilagođeni kôd za manipulaciju izvornim podacima u skladu s našim zahtjevima prije nego što ga zapišemo u odredišni objekt.

Pokrenimo test kako bismo potvrdili da naš prilagođeni mapper radi:

@Test javna praznina givenSrcAndDest_whenCustomMapperWorks_thenCorrect () {mapperFactory.classMap (Personne3.class, Person3.class) .customize (customMapper) .register (); MapperFacade mapper = mapperFactory.getMapperFacade (); String dateTime = "2007-06-26T21: 22: 39Z"; duga vremenska oznaka = novo Long ("1182882159000"); Personne3 personne3 = nova Personne3 ("Leornardo", vremenska oznaka); Person3 person3 = mapper.map (personne3, Person3.class); assertEquals (person3.getDtob (), dateTime); }

Primijetite da prilagođeni mapir i dalje prosljeđujemo Orikinom mapperu putem ClassMapBuilder API, baš kao i sva ostala jednostavna prilagođavanja.

Možemo također potvrditi da dvosmjerno mapiranje djeluje:

@Test javna praznina givenSrcAndDest_whenCustomMapperWorksBidirectionally_thenCorrect () {mapperFactory.classMap (Personne3.class, Person3.class) .customize (customMapper) .register (); MapperFacade mapper = mapperFactory.getMapperFacade (); String dateTime = "2007-06-26T21: 22: 39Z"; duga vremenska oznaka = novo Long ("1182882159000"); Person3 person3 = nova Person3 ("Leornardo", dateTime); Personne3 personne3 = mapper.map (person3, Personne3.class); assertEquals (person3.getDtob (), vremenska oznaka); }

8. Zaključak

U ovom članku imamo istražio najvažnije značajke okvira za mapiranje Orika.

Definitivno postoje naprednije značajke koje nam daju puno više kontrole, ali u većini slučajeva korištenja, ovdje obrađene bit će više nego dovoljne.

Kompletni kod projekta i svi primjeri mogu se naći u mom github projektu. Ne zaboravite pogledati i naš vodič o okviru Dozer mapping, jer oboje rješavaju manje-više isti problem.