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:
@Repositoryhaszná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ítjaDataAccessExceptionkivételekké. MappingSqlQuery<T>: A lekérdezéseket maga aMappingSqlQueryismeri, 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 (KeyHolderhasználatával).BatchSqlUpdate: Adatok mentése batch-elve.SqlFunction<T>: Tárolt eljárások hívását egyszerűsíti