Spring JPA¶
Az előző fejezetben megmutattuk, hogyan lehet használni a Hibernate-et a Session
és a SessionFactory
segítségével.
Egy másik, talán kényelmesebb használati mód, ha a Hibernate-et standard Java Persistent API (JPA)-val használjuk.
JPA használatával azt az előnyt is megkapjuk, hogy a standardizált elemeket egységesen tudjuk használni akármilyen JPA provider-t is alkalmazunk a motorháztető alatt (OpenJPA, EclipseLink, Hibernate, Toplink, stb.).
A sima JPA előnyök mellett a Spring remek support-ot ad a JPA támogatáshoz (Spring Data project).
JPA bevezetés¶
Először pontosítsuk is, hogy mi az a JPA. A JPA egy specifikáció, melyet standardizálja az ORM technikák használatát mind JSE és JEE környezetben. Definiálja a használható koncepciók, annotációk, interfészek és egyéb szolgáltatások halmazát, melyeket a JPA provider-eknek meg kell kell valósítaniuk. Ezáltal a fejlesztők bármikor szabadon megváltoztathatják a rendszerben a JPA provider-t mindenféle szenvedés nélkül.
JPA-n belül az egyik központi szereplő az EntityManager
interface, melyet az EntityManagerFactory
-nak kell biztosítania.
Az EntityManager
feladata perzisztencia kontextus menedzselése, melyen keresztül történik az összes entitás objektum menedzselése is (tehát a DB műveleteket ezen keresztül végezhetjük majd el).
Ha megfeleltetést kell tennünk, akkor az EntityManager
megfelel a Session
interface-nek, az EntityManagerFactory
pedig a SessionFactory
-nak.
A fő különbség, hogy JPA-ban nem tudunk direkt módon interakcióba kerülni a kontextussal, hanem az EntityManager
-re bízzuk a nehéz melót.
Az előző fejezetben saját lekérdezéseinkhez a HQL-t (Hibernate Query Language) használtuk, de a JPA ezt is szabványosította, melyet JPQL(Java Persistence Query Language)-nek hívnak.
A JPA 2 nagy újdonsága még az erősen típusos Criteria API, mely lehetővé teszi a fordítási időben történő hiba ellenőrzést.
Jelen pillanatban a JPA 2.2 szabvány a legfrissebb, mely támogatást ad a stream API használatához, a Java 8-as Date
és Time
típusokhoz és még néhány további hasznos elemet is biztosít.
EntityManagerFactory konfiguráció¶
Springben 3 lehetséges módja van az EntityManagerFactory
konfigurálásának:
LocalEntityManagerFactoryBean
: a legegyszerűbb megvalósítás, de nem támogatja aDataSource
injektálását, így nem tudjuk tranzakciókezelésben sem alkalmazni.- JEE kompatibilis konténerekben, melyek bootstrappelik a JPA perzisztenciát biztosító komponeneseket (deployment descriptor-ban). Ilyenkor JNDI lookup-al szerezhetünk referenciát az
EntityManager
-re. A deployment descriptor hagyományosan aMETA-INF/persistence.xml
volt, de ez Spring 3.1-el megváltozott, pontosabban már nem szükséges így eljárnunk. LocalContainerEntityManagerFactoryBean
: támogatja aDataSource
injektálást is. Ez a legáltalánosabban használt módszer, melyre rögtön egy példát is mutatunk.
Mielőtt belevágunk a konfigurációba be kell állítanunk a pom.xml
-ben a függőségeinket!
Amennyiben szeretnénk a Spring Boot-ot JPA-val, illetve annak Hibernate megvalósításával használni, akkor a pom.xml
-ben a következő függőséget kell elhelyezni (H2 drivert is megadjuk):
1 2 3 4 5 6 7 8 |
|
A spring-boot-starter-data-jpa
automatikusan a Hibernate-et használja, mint JPA provider.
Nézzük is, hogy milyen konfigurációkat kell megtennünk ahhoz, hogy az alkalmazásunk használhassa a Spring JPA adta lehetőségeket.
Ehhez tekintsük meg a JpaConfig
konfigurációs osztályt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
Nézzük, hogy mi milyen célt szolgál ebben a konfigurációs állományban!
A dataSource
szerepét már megismertük.
A transactionManager
-t később nézzük meg alaposabban, viszont muszáj erre is kitérnünk, hiszen az EntityManagerFactory
követel magának egy tranzakció kezelőt.
Szerencsére a Spring ad is számunkra egy ilyen tranzakciókezelőt: org.springframework.orm.jpa.JpaTransactionManager
.
Az entityManagerFactory
bean a legfontosabb a fenti bean definíciók közül.
Az entityManagerFactory
-nak tudnia kell, hogy melyik DataSource
-ot kell használnia, hogy melyik vendor-t akarjuk használni (Hibernate-et), hogy hol kell keresni az entity-ket, hogy milyen beállításokat továbbítson a JPA provider (Hibernate) felé.
Megjegyzés
Spring Boot használatakor a fentieket mind el is hagyhatjuk, hacsak nem szeretnénk valamit egyedileg konfigurálni (illetve lehetőség van az application.properties
fájlban is néhány beállítást megadni).
JPA használata ORM leképezéshez¶
Mivel az előző fejezetben az entitás osztályainkat a javax.persistence
annotációkkal láttuk el, így nincs további teendőnk ezekkel (alapból JPA kompatibilis).
Miután megvagyunk a konfigurációval használhatjuk az EntityManagerFactory
-t, hogy egyszerűen injektáljuk oda, ahol szükség van rá.
Például az előző fejezetben használt CustomerDaoImpl
osztályban használhatjuk a következőképpen:
1 2 |
|
A @PersistenceContext
annotáció egy standard JPA annotáció az EntityManager
injektálására.
Alapvetően a default
persistence unit-ot fogja használni ez az EntityManager
.
Egy persistence unit-ot igazából meg lehet feleltetni egy-egy dataSource-nak.
Ez akkor jöhet jól, ha az alkalmazásunk egyszerre több adatbázissal is kommunikál.
Ilyen esetben a unitName
megadásával adhatjuk meg a használni kívánt persistence unit nevét.
Ebben az esetben a findAll
megvalósítása:
1 2 3 4 |
|
Ebben az esetben a findAll
az EntityManager
-t használja, melynek a createNamedQuery
metódusát tudjuk meghívni egy nevesített lekérdezés előkészítésére (paramétereket is beállíthatunk rajta), majd a getResultList()
segítségével elkérhetjük az eredménylistát.
Fontos
A JPA előírja, hogy az összes fetch
-elés EAGER
módon történik alapvetően (amikor nincs ez explicit megadva), ugyanakkor a Hibernate még így is a lazy fetching technikát alkalmazza.
Amikor a createNamedQuery
-nek megadjuk második paraméterben azt is, hogy milyen típusú eredményt várunk vissza, akkor ő egy TypedQuery<T>
típusú objektummal tér vissza, mely segíti a fordítás közbeni típusellenőrzéseket.
Egy ilyen TypedQuery<T>
-re hívva a getResultList()
-et az eredmény nyilván List<T>
típusú lesz.
Előfordulhat olyan eset is, amikor szándékosan nem akarjuk lemappelni az eredményt egy entity-re, mivel nem is tudnánk.
Például, amikor több táblából gereblyézzük össze egy riporthoz az információkat, akkor annak eredményét nagy valószínűséggel nem fogjuk tudni megfeleltetni egy entitásnak.
Ilyenkor az ún. Untyped
lekérdezéseket használjuk.
Például:
1 2 3 4 5 6 7 |
|
Ilyen esetben a createQuery()
egy sima Query
objektumot ad vissza.
Ezen az eredményen aztán végiglépkedhetünk egy iterátor segítségével, mely minden rekordra egy-egy Object
tömböt ad vissza, mely a lekérdezett oszlopokat reprezentálja.
A fenti megoldás nem túl szép, de szerencsére van másik megoldás rá.
Kérhetjük, hogy a fenti Object
tömb helyett a JPA konstruáljon egy POJO-t.
Ezt a POJO-t view-nak hívják, mivel több tábla adatait foglalja magába (hasonlóan, mint amikor az adatbázis view-kat használjuk).
Ehhez csinálnunk kell egy POJO osztályt:
1 2 3 4 5 6 7 |
|
A reportáláshoz lehet csinálni egy külön service-t:
1 2 3 |
|
melyet aztán megvalósíthatunk egy tényleges implementáló osztályban:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Figyeljük meg a JPQL lekérdezésben a new
kulcsszót!
Ezt hívják constructor extpression-nek, aminek meg is mondjuk, hogy milyen paraméterekkel kell meghívni a konstruktort, és ez alapján elő is állnak ezek a típusú objektumok.
Nyilván egy TypedQuery<Summary>
típusú objektumot ad vissza a createQuery
ilyen esteben.
CRUD műveletek¶
JPA segítségével a következőképpen végezhetjük el a mentést:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
A különbség csak annyi, hogy itt nekünk kell szétválasztani a mentés operációt új és update esetekre, melyet az id alapján végzünk el.
A törléshez is egyértelmű műveletet kínál az EntityManager
, mégpedig a remove()
metódust.
Megjegyzés
Törlés előtt érdemes lehet egy merge()
hívást megejteni, hogy az asszociációkat updateljük még mielőtt kitöröljük az entitást. Nyilván, ha csak kaszkádolt törlések vannak megadva mindenhol, akkor ez a része nem feltétlenül lehet szükséges.
Natív lekérdezések¶
Amikor valamilyen vendor-specifikus (nem támogatott csak egy adott DB által) lekérdezést szeretnénk használni, akkor nincs más lehetőségünk, mint egy natív lekérdezést használni, melyet a JPA szimplán továbbpasszol az adatbázis szervernek úgy ahogy van (mapping és transzformáció nélkül) és a többit majd intézi maga az adatbázis.
Ettől függetlenül magát az eredményt (ResultSet
) vissza tudjuk mappelni az entity-khez.
Példa:
1 2 3 4 5 6 7 8 |
|
A fentiek mellett lehetőség van arra is, hogy az SQL ResultSet
mapping-et megadjuk, melyet az entity osztályon kell jeleznünk.
Egy egyszerű példa:
1 2 3 4 |
|
A JPA támogatja több entity használatát és az oszlop-szintű mapping-et.
Ezután ezt az SqlResultSetMapping
-et a következőképpen használhatjuk fel:
1 2 3 4 5 6 7 8 9 |
|
Criteria API¶
A Criteria API akkor jön nagyon jól, amikor kereséseket szeretnénk végezni valamilyen field-ek alapján. Ahelyett, hogy rengeteg kombinációt adunk meg (keresés csak név alapján, csak id alapján, csak létrehozás dátuma alapján, stb.), használhatjuk a Criteria API-t, mellyel ez sokkal szebben megoldható.
A JPA a 2.0-ás verziótól támogatja az erősen típusos lekérdezéseket a Criteria API használatával, melyet a Metamodel API segítségével tudunk megvalósítani.
A lekérdezéseket az entity osztály meta-modelljén fogalmazhatjuk meg, melyet a class neve mögé fűzött _
(underscore)-ral kaphatjuk meg.
Példa:
1 2 3 4 5 6 7 8 9 10 11 |
|
Az osztály a @StaticMetamodel
annotációval kell ellátnunk, melynek attribútumában megadjuk a mappelt entitást.
Az osztályon belül az összes kereshető attribútumhoz megadjuk a megfelelő keresés lehetőséget, melyeken a típust is előírjuk (mindegyik használt attribútumnak Attribute
típusúnak kell lennie).
Ennek az egész Metamodel API
-nak a lényege az, hogy a keresési feltételeinket ne stringekkel adjuk meg, hanem azokat típusosan tudjuk kezelni.
Ez a fenti metamodell viszont nem igazán tűnik jónak, mert ezt is karban kell tartani, ugyanakkor nagyon egyszerű szerkezetű, ami adja, hogy léteznek hozzá generátorok, mint például a hibernate jpamodelgen
.
Ehhez a következőt kell a pom.xml
-be írnunk:
1 2 3 4 |
|
A generátor a target/generated-sources
alá helyezi el a legenerált metamodelleket.
Fontos
Mivel a generált állományok a target/generated-sources
alá kerülnek be, így ezt hozzá kell adnunk a classpath-hoz, máskülönben nem fogja fejlesztés közben feloldani az IDE a hivatkozásokat.
Miután ezzel megvagyunk, definiáljunk egy List<Customer> findByCriteriaQuery(String name, Date before)
alakú lekérdezést, ahol a vásárló nevének pontosan egyeznie kell a megadott name
-mel, illetve a születésnapja a megadott dátum előtt van!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Vegyük sorra, hogy mit tettünk a fenti lekérdezésben:
- Először is referenciát kell szereznünk a
CriteriaBuilder
-re, melyet azEntityManager
-től kérhetünk el. - A
createQuery()
visszaad egyCriteriaQuery<Customer>
objektumot, azaz erősen típusos lekérdezést - A lekérdezés gyökér objektumát a
Customer
entitásra adjuk meg, azaz az olyan feltételek, melyekben útvonal kifejezések szerepelnek, innen fognak indulni. Ez mindig valamilyen entitás kell legyen - a
Root<T>.fetch
hívások az asszociációk menti EAGER lekérdezéseket adják meg. - A
CriteriaQuery.select()
lényegében megmondja, hogy melyik táblából kell majd a select-et véghezvinnünk, illetve az eredményDISTINCT
legyen (duplikált elemek szűrése). - Egy
Predicate
példányt kapunk, ha aCriteriaBuilder.conjunction()
metódust meghívjuk, ami egy vagy több feltétel együttes teljesülését várja el. A feltételeket mind egy-egyPredicate
hivatott megadni. - Ezután rendre megnézzük, hogy kell-e további feltételeket (
Predicate
) hozzáadni a keresési kritériumokhoz - A keresési feltételeket a
criteriaQuery
where
metódusában adhatjuk át. - Végül lefuttatjuk a lekérdezést és visszaadjuk annak eredményét
Amennyiben a customer
nevének nem kell teljes egyezést adnia, akkor használhatjuk a cb.like(customerRoot.get(Customer_.name), name);
alternatívát is.
Repository¶
Az eddigiek pusztán JPA specifikus megoldások voltak.
A Spring Data segítségével használhatjuk az igen erős Repository
absztrakciót, mely tovább egyszerűsíti az adatbázis műveleteket.
Ez az absztrakció magában foglalja a JPA EntityManager-ét is.
A központi interfész, melyet használhatunk a org.springframework.data.repository.Repository<T,ID extends Serializable>
.
Ehhez az alap interface-hez számos bővítés tartozik, melyek közül az egy a CrudRepository
interface.
A CrudRepository
a következő műveleteket adja (forráskódból kiemelt):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Ezeket a metódusokat alapból használhatjuk, viszont amennyiben egyéb lekérdezésre van szükségünk úgy további metódusokat adhatunk az interfészhez:
1 2 3 4 |
|
A csodás dolog az inteface-hez hozzáadott metódusokban az, hogy ha néhány névkonvenciót követünk, akkor semmilyen lekérdezést nem kell megadnunk.
A Spring Data JPA implementációja ki tudja találni magát a lekérdezést a metódus neve alapján, illetve ismeri azt is, hogy egy CrudRepository<Customer, Long>
-val van dolga, azaz tudja, hogy a Customer
táblára vonatkoznak a megadott lekérdezések.
A findByName
metódusból például a következő lekérdezést rakja össze a rendszer: select c from Customer c where c.name = :name
, továbbá be is állítja a paramétert a megadott paraméterre, melynek neve megegyezik a nevesített paraméter nevével.
A CrudRepository
-nál létezik egy fejletteb interface, melyet JpaRepository
-nak hívnak és az alapműveletek mellett támogatja a batch, paginálás és rendezés műveleteket is.
Láttuk, hogy a megadott metódusokból a rendszer kitalálja a lekérdezést magát.
Vannak helyzetek, amikor viszont ennyi nem elég és mégis valami egyedi lekérdezést szeretnénk írni.
Ilyen esetben magát a lekérdezést a @Query
annotáció használatával adhatjuk meg.
Például a vásárló név alapú keresését így is megadhatom:
1 2 |
|
Abban az esetben, ha a nevesített paraméter neve megegyezik a metódus paraméterének nevével akkor nincs szükség a @Param
használatára.
Változások követése az Entity osztályon¶
Egy alkalmazásban általában követnünk kell bizonyos tevékenységeket, melyek az entitásokat érintik, mint például létrehozás dátuma, utolsó módosítás dátuma, ki módosította utoljára, stb.
Ezek támogatására a Spring Data szolgáltatja számunkra JPA entity listener-eket, melyek segítségével automatizálhatjuk ezen tevékenységeket is.
Spring 4 és azelőtt elég sokat kellett ehhez dolgozni, mivel implementálni kellett az Auditable<U, ID extends Serializable, T extends TemporalAccessor> extends Persistable<ID>
interfészt, melynek 8 metódusát is ki kellett fejteni.
Spring 5-ben szerencsére megváltozott ez és mindent tudunk annotációval szabályozni:
@CreatedBy
@CreatedDate
@LastModifiedBy
@LastModifiedDate
Az Entity-re rá kell aggatnunk az @EntityListeners(AuditingEntityListener.class)
annotációt, hogy támogassuk erre az entitásra a fenti műveleteket.
Olyan esetben, ha több osztályban is szeretnénk alkalmazni a fenti auditálásokat, akkor érdemes egy külön osztályba ezt kiszervezni, melyből aztán a többi entity származik:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
A @MappedSuperclass
-ra azért van szükség, hogy az itt megadott fieldek is szerves részét képezzék a leszármazott osztály adattagjainak és a mapping során ezek a field-ek is leképződjenek a tábla megfelelő oszlopaira.