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
private static Logger logger = LoggerFactory.getLogger(SimpleDao.class);

static {
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
    } catch (ClassNotFoundException ex) {
        logger.error("Prblem loadng DB Diver!", ex);
    }
}

private Connection getConnection() throws SQLException {
    return DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb?useSSL=true", "user", "user");
}

private void closeConnection(Connection connection) {
    if (connection == null) {
        return;
    }
    try {
        connection.close();
    } catch (SQLException ex) {
        logger.error("Problem closing connection to the database!",ex);
    }
}

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
public List<Contact> findAll() {
    List<Contact> result = new ArrayList<>();

    Connection connection = null;

    try {
        connection = getConnection();
        PreparedStatement statement = connection.prepareStatement("select * from contact");
        ResultSet resultSet = statement.executeQuery();
        while (resultSet.next()) {
            Contact contact = new Contact();
            contact.setId(resultSet.getLong("id"));
            contact.setName(resultSet.getString("name"));
            contact.setBirthDate(resultSet.getDate("birth_date"));
            result.add(contact);
        }
        statement.close();
    } catch (SQLException ex) {
        logger.error("Problem when executing SELECT!",ex);
    } finally {
        closeConnection(connection);
    }
    return result;
}

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
@Configuration
public class JdbcConfig {
    @Bean
    public DataSource mysqlDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/testdb");
        dataSource.setUsername("user");
        dataSource.setPassword("user");

        return dataSource;
    }
}

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
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

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
spring.datasource.url=jdbc:mysql://localhost:3306/testdb
spring.datasource.username=user
spring.datasource.password=user

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
@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
      .setType(EmbeddedDatabaseType.H2)
      .addScript("classpath:schema.sql")
      .addScript("classpath:data.sql").build();
}

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
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

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
spring.datasource.schema=db/schema.sql
spring.datasource.data=db/test-data.sql

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
...
private DataSource dataSource;
private JdbcTemplate jdbcTemplate;

@Autowired
public SimpleContactDao(DataSource dataSource) {
    this.dataSource = dataSource;
    jdbcTemplate = new JdbcTemplate(dataSource);
}
...

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
@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:schema.sql")
            .addScript("classpath:data.sql").build();
}

@Bean
public JdbcTemplate jdbcTemplate(){
    return new JdbcTemplate(dataSource());
}

Ezután a dao-ban a következőt tehetjük:

1
2
3
4
5
@Autowired
public SimpleContactDao(DataSource dataSource, JdbcTemplate jdbcTemplate) {
    this.dataSource = dataSource;
    this.jdbcTemplate = jdbcTemplate;
}

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
public String findNameById(Long id) {
    return jdbcTemplate.queryForObject(
            "select name from contact where id = ?",
            new Object[]{id}, String.class);
}

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
public String findNameById(Long id) {
    Map<String, Object> params = new HashMap<>();
    params.put("contactId", id);
    return namedParameterJdbcTemplate.queryForObject(
            "select name from contact where id = :contactId",
            params, String.class);
}

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
private RowMapper<Contact> contactRowMapper = new RowMapper<Contact>() {
    @Override
    public Contact mapRow(ResultSet rs, int rowNum) throws SQLException {
        Contact contact = new Contact();
        contact.setId(rs.getLong("id"));
        contact.setName(rs.getString("name"));
        contact.setBirthDate(rs.getDate("birth_date"));

        return contact;
    }
};

@Override
public List<Contact> findAll() {
    return jdbcTemplate.query("SELECT * FROM Contact", contactRowMapper);
}

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ítja DataAccessException kivételekké.
  • MappingSqlQuery<T>: A lekérdezéseket maga a MappingSqlQuery 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

Utolsó frissítés: 2020-09-28 07:00:05