JDBC (Java DataBase Connectivity)¶
A JDBC egy szabványos eszközt ad a fejlesztők kezébe, amin keresztül az adatbázis felé műveleteket tudnak végezni. A JDBC magja egy driver, mely minden adatbázishoz külön-külön implementál maga az adatbázis gyártója, azaz a vendor (Oracle, Postgres, MySQL, stb.). Ezek a driverek teljesítik a JDBC interfész specifikációit, így Java kódon belül egységesen tudjuk kezelni az összes adatbázis, melynek előnye, hogy menet közben is egyszerűen kicserélhetjük a DB-t a rendszer alatt.
Java-n belül a driverek betöltését a java.sql.DriverManager
végzi el.
A DriverManager
a driverek egy listáját képes egyszerre menedzselni.
A DriverManager.getConnection()
statikus metódushívással kaphatunk referenciát egy másik központi elemre, a java.sql.Connection
-re.
A kapcsolati objektumon keresztül lehetőségünk van lekérdezéseket, utasításokat adni az adatbázisnak.
Bár a JDBC egy kiforrott technika, azért vannak hátulütői:
- A fejlesztőnek magának kell menedzselnie a kapcsolatot az adatbázis felé. Mivel a kapcsolat felállítása drága dolog, így azt jól kell csinálni, ami nem biztos, hogy mindig sikerül.
- Új kapcsolat létrehozásakor egy új szál vagy egy child process jön létre, melyek száma sok esetben korlátozva van
- Az SQL utasítások futtatása igen körülményes
Nézzük is, hogy mire lesz szükségünk, amikor kapcsolatot szeretnénk létrehozni, illetve bontani:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Látszik, hogy csak a kapcsolat felépítése és bontása is tele van hibaforrással, amit megfelelően kezelni kell.
A fenti kód még nem is végzett semmilyen műveletet a DB felé.
Nézzük meg, hogy hogyan lehetne például kinyerni az adatbázisból az összes sor tartalmát, illetve azokat POJO-kká alakítani (itt feltételezzük, hogy létezik egy számunkra pontosan megfelelő Contact
tábla a testdb
-ben)!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
A fenti kód egyértelműen mutatja azt, hogy a lekérdezés milyen bonyolult tud lenni.
Kicsit segíthetünk a kivételkezelésen, ha a try-with-resource
nyelvi konstrukciót is használjuk, de ettől még nem lesz minden megoldva.
Ami még rosszabb, hogy amint több funkcionalitást adunk a rendszerbe, úgy kezdjük el duplikálni a kódot is, melyet karbantartani rémálom lesz.
Persze elkezdhetjük kiszervezni a kódot külön helper-ekbe, de attól még sok felesleges dolgot kell csinálnunk.
Spring JDBC¶
A Spring rendelkezik JDBC támogatással, mely a fent látott terheket megpróbálja levenni a fejlesztő válláról.
Spring JDBC felépítése¶
TODO:
Kapcsolódás és DataSource¶
A kapcsolatok menedzselését teljes egészében a Spring-re bízhatjuk, melyhez egy bean-t kell csak biztosítanunk a keretrendszer számára.
A bean-nek a javax.sql.DataSource
interfészt kell implementálnia.
Ez a DataSource
bean menedzseli a Connection
objektumokat.
A legegyszerűbb megvalósítása ennek az interface-nek a DriverManagerDataSource
(org.springframework.jdbc.datasource
alatt).
A nevéből látszik, hogy egyszerűen a DriverManager
-en keresztül végzi el ezt a feladatot.
Ugyanakkor a DriverManagerDataSource
nem támogatja a connection pooling-ot, ami nem teszi ideálissá a használatát.
Connection Pooling
A connection pooling az adatbázis kapcsolatokat cache-eli, így azok újrafelhasználhatóak maradnak, amikor a jövőben újabb kérést kell kiszolgálni. Használatuk nagyban növeli a teljesítményt, hiszen az adatbázis műveleteknél a legdrágább művelet maga a kapcsolat felépítése. A connection pooling esetében egy kapcsolat létrehozásakor a kapcsolat belekerül a "pool"-ba (ami igazából a cache), és új művelet esetén ebből a poolból kapunk egy használaton kívülit. Amennyiben az összes poolban lévő kapcsolat foglalt, akkor egy új kapcsolatot hozunk létre, majd azt is a pool-ban helyezzük el miután az adott kérést kiszolgáltuk a segítségével.
Egy példa a DataSource
bean létrehozására:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Ahhoz, hogy fenti példa függőségeit elérjük a pom.xml
-ben szerepelnie kell a következőnek:
1 2 3 4 |
|
Megjegyzés
Az adatbázishoz tartozó dataSource
tulajdonságait az application.properties
-ben is be tudjuk állítani, amennyiben Spring Boot-ot használunk.
Például:
1 2 3 |
|
Miután a fentieket hozzáadtuk konfigurációinkhoz a Spring automatikusan ezt a bean-t fogja használni az adatbázis csatlakozás felépítéséhez.
Megjegyzés
A Spring Boot amennyiben megtalálja a H2-t a classpath-ban, akkor nincs is szükség a dataSource
bean megadására, mivel ilyenkor automatikusan egy beépített H2 dataSource
-ot használ és ezt a bean-t regisztrálja is.
Embedded DataSource¶
Spring 3.0-tól lehetőségünk van beágyazott DB használatára, melynek során automatikusan elindul egy beágyazott adatbázis és az automatikusan regisztrálódik a dataSource
bean-ként is.
Egy népszerű megoldást kínál a H2 adatbázis, melyet elindíthatunk ilyen módon is (van lehetőség Derby és a HSQL (ez az alapértelmezett) használatára is).
1 2 3 4 5 6 7 |
|
Ahhoz, hogy a H2-t haszálhassuk szükségünk van annak DB driver-ére, amit a classpath-ba kell raknunk, így a pom.xml
-hez adjuk hozzá a következő dependency-t!
1 2 3 4 |
|
Az Embedded DB nagyon hasznos tud lenni fejlesztés közben, illetve teszteléskor is, mivel nincs hozzá szükség semmilyen DB telepítésre vagy további függőségekre. A fenti példában azon felül, hogy elindítom a beágyazott adatbázisomat, lefuttatom a DDL (Data Definition Language) utasítások (tábla létrehozások), illetve futtatok egy DML szkriptet is, melynek segítségével feltöltöm az adatbázist kezdeti adatokkal.
Megjegyzés
Spring Boot használatakor a schema.sql
és a data.sql
(fontos a név) állományokat a rendszer automatikusan lefuttatja.
Ezeket a resources
mappa gyökerében keresi, szóval a fenti esetben a szkript megadásokat el is hagyhatjuk.
Amennyiben a szkripteket más helyen tároljuk, akkor használhatjuk az application.properties állományt, hogy megadjuk ezek pontos helyét, illetve nevét:
1 2 |
|
JdbcTemplate
¶
A JdbcTemplate
a Spring JDBC támogatás központi eleme, mely tetszőleges SQL utasítást képes lefuttatni legyen az DDL-specifikus vagy DML-specifikus.
A JdbcTemplate
-et általában a dataSource
-al együtt szokták inicializálni (injektálás helyén), mivel így ő is egyből készen áll a bevetésre.
Példa:
1 2 3 4 5 6 7 8 9 10 |
|
A JdbcTemplate
szálbiztos, ami azt jelenti, hogy csinálhatjuk azt is, hogy csak egy darab JDBC template-et hozunk létre bean formájában és azt injektáljuk mindenhova:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Ezután a dao-ban a következőt tehetjük:
1 2 3 4 5 |
|
Nyilván setter dependency injection-t is használhattunk volna.
Megjegyzés
A Spring Boot automatikus konfigurációja során, ha a classpath-ban megtalálja a spring-boot-starter-jdbc
függőségeit, akkor automatikusan regisztrálja a jdbcTemplate
bean-t, így azt el is távolíthatjuk a konfigurációs osztályból.
A következő példában nézzük meg, hogy egy egyszerű értéket, hogyan adhatunk vissza a JdbcTemplate
segítségével.
Ehhez elkészítünk egy findNameById
metódust, mely visszaadja a Contact
nevét az id
-ja alapján:
1 2 3 4 5 |
|
A queryForObject
hívásban az első paraméter maga az SQL utasítás, melyben paramétereket is megadhatunk a ?
segítségével.
A második paraméterében megadunk egy objektum tömböt, amiben az összes paraméter értékét felsoroljuk (sorrend szerint fognak behelyettesítődni).
A harmadik paraméterben pedig megadjuk azt, hogy az eredmény, amit várunk az milyen típusú, így az SQL utasítás eredményét automatikusan át tudja alakítani a template.
Az egyszerű ?
paraméter placeholder, helyett lehetőségünk van arra is, hogy nevesített paramétereket adjunk meg a JdbcTemplate
egy specializációjával, a NamedParamterJdbcTemplate
-el.
Ugyanúgy hozható létre, mint a sima JdbcTemplate
, szóval készíthetünk hozzá egy bean-t és aztán injektálhatjuk a DAO osztályba (amennyiben spring-boot-starter-jdbc
-t használunk, akkor NamedParameterJdbcTemplate
bean-t is automatikusan létrehoz a rendszer).
Az előző példa nevesített paraméterek használatával:
1 2 3 4 5 6 7 |
|
Amikor nevesített paramétereket használunk, akkor a paraméterek nevét prefixáljuk egy :
-al, például :contactId
.
Amint látható, ilyen esetben egy Map
-et kell megadnunk a queryForObject
-nek, melyben <param_név, érték>
formában helyezkednek el a paraméterek.
RowMapper¶
A legtöbb esetben nem pusztán egy értéket akarunk lekérdezni, hanem egy teljes sort, amelyet aztán átalakítunk a megfelelő domain objektummá (entity).
Ehhez nagyszerűen alkalmazható a Spring által nyújtott RowMapper<T>
interface.
A használatához implementáljuk a findAll()
metódust a ContactDao
-n!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
A RowMapper
a tradicionális ResultSet
-et kapja meg a mapRow
metódusában, illetve a sor indexét is rendelkezésünkre bocsájtja a rendszer.
Itt egyszerűen elvégezzük a konverziót, majd visszaadjuk a contact
-ot.
Amikor a query
metódusok sok-sok megvalósítása közül egy olyat választunk, ami RowMapper<T>
-t kap paraméterül, akkor az mindig List<T>
-t fog visszaadni (a queryForObject
ilyen esetben is csak egy T
objektumot ad vissza, nem pedig azok listáját).
Megjegyzés
Amikor bonyolultabb táblaszerkezetünk van és nem csak egy táblát akarunk POJO-vá alakítani, hanem mondjuk egy kulcson keresztül egy másik tábla adatait is szeretnénk lekérdezni, akkor használhatjuk a ResultSetExtractor
interface-t.
JDBC operációkhoz tartozó osztályok¶
Láthattuk, hogy a JdbcTemplate
és a hozzá kapcsolódó segédosztályok segítségével tetszőleges SQL utasításokat adhatunk, illetve konvertálhatjuk a visszakapott információt OO környezetbe.
Ezen felül a Spring biztosít további komponenseket, melyek még közelebb viszik az OO paradigmához az adatbázis kezelését.
Ezek a következők:
- Dao építés annotációval:
@Repository
használata a DAO implementáción, melynek eredményeképpen nem csak bean lesz az adott osztályból, de a natív SQL kivételeket a rendszer automatikusan átalakítjaDataAccessException
kivételekké. MappingSqlQuery<T>
: A lekérdezéseket maga aMappingSqlQuery
ismeri, továbbá ismernie kell a dataSource objektumot is. Viszont ezen a módon a lekérdezéseket külön osztályokban tudjuk megfogalmazni, ami újrafelhasználhatóság szempontjából remek, illetve közelebb visz az OO-hoz is.SqlUpdate
: Hasonlóan működik, mint az előző, csak update-re, továbbá támogatja a nevesített paraméterek használatát és a beszúrt elem id-jának lekérdezését (KeyHolder
használatával).BatchSqlUpdate
: Adatok mentése batch-elve.SqlFunction<T>
: Tárolt eljárások hívását egyszerűsíti