JDBC

Z FI WIKI
Přejít na: navigace, hledání

JDBC (Java Database Connectivity) je univerzální aplikační rozhraní pro přístup k relačním databázím. Je standardní součástí Java SE. Pro jeho používání je potřeba pouze JDBC ovladač (driver).

Teoreticky umožňuje psát aplikace nezávislé na konkrétním systému řízení báze dat (SŘBD), nicméně problémem samozřejmě zůstává nekompatibilita jednotlivých SŘBD na úrovni jazyka SQL.

JDBC je poměrně nízkoúrovňové rozhraní, což komplikuje jeho používání. V praxi se proto obvykle nepoužívá přímo, ale buď se využívá nějaký ORM nástroj (viz Java Persistence API), nebo se použije nějaká nadstavbová knihovna (např. Spring JDBC).

Práce s JDBC

Navázání spojení

Nejdříve musíme přimět zavaděč tříd, aby zavedl třídu požadovaného JDBC ovladače. Toho se dá dosáhnout několika způsoby, nejjednodušší je použít metodu Class.forName(java.lang.String):

Class.forName("com.mysql.jdbc.Driver");

Příslušný JDBC ovladač samozřejmě musí být v CLASSPATH. Od javy verze 1.6 je k dispozici mechanismus automatického zavádění služeb a pokud máme k dispozici dostatečně novou verzi JDBC ovladače (která má správně nastavená příslušná metadata), k jeho zavedení dojde automaticky a předchozí krok můžeme vynechat (ovladač ale samozřejmě musí být stále v CLASSPATH).

Spojení navážeme pomocí třídy DriverManager a její metody DriverManager.getConnection(java.lang.String). Parametr url obsahuje informace potřebné pro připojení k databázi.

String url="jdbc:mysql://localhost:3306/database?useUnicode=true";
Connection conn = DriverManager.getConnection(url,"user","password");

Jméno třídy a formát URL nalezneme v dokumentaci k příslušnému JDBC ovladači. Pozor! Nesmíme zapomenout spojení vždy uzavřít! I v případě, že dojde k chybě!

Tento způsob navázání spojení je nejjednodušší, ale má svá úskalí (musíme znát konfiguraci pro připojení k databázi). Častěji se proto pro navazování spojení používá rozhraní DataSource (viz níže).

Provádění SQL příkazů

Máme-li vytvořené spojení, můžeme provádět SQL příkazy. K tomu potřebujeme instanci Statement:

Statement st = conn.createStatement();

Nyní můžeme databázi posílat příkazy pomocí metody Statement.execute(java.lang.String):

boolean result = st.execute("SELECT * FROM myTable;");

Výsledkem provedeného příkazu může být buď relace (tabulka), nebo jde pouze o aktualizaci dat a výsledkem je počet změněných řádků. V prvním případě metoda execute(String) vrátí true a výsledek operace je možné získat pomocí metody Statement.getResultSet():

ResultSet resultSet = st.getResultSet();

V druhém případě metoda execute(String) vrátí false a počet aktualizací je možné získat pomocí metody Statement.getUpdateCount():

int updatesCount = st.getUpdatesCount();

Pokud víme předem, že bude výsledkem relace, můžeme použít metodu Statement.executeQuery(java.lang.String), která vrací přímo danou relaci. Pokud naopak víme, že jde o aktualizaci dat, můžeme použít metodu Statement.executeUpdate(java.lang.String), která vrátí přimo počet změněných řádků:

ResultSet resultSet = st.executeQuery("SELECT * FROM myTable;");
int updatesCount = st.executeUpdate("DELETE FROM myTable WHERE a = 1;");

Zpracování výsledků

Výsledek SQL dotazu je reprezentován rozhraním ResultSet. Typické schéma zpracování je následující:

while (resultSet.next()) {
    int a = resultSet.getInt(1);
    String b = resultSet.getString(2);
    boolean c = resultSet.getBoolean(3);
    // ...
}

Pozor! Pořadí parametrů je číslováno od jedné (narozdíl např. od polí v Javě)!

Pokud chceme ke sloupcům přistupovat podle jménem, můžeme použít metodu ResulSet.findColumn(java.lang.String) nebo místo parametru typu int s pořadím atributu použijeme parametr typu String se jménem atributu.

int indexColumnA = resultSet.findColumn("columnA");
while (resultSet.next()) {
    int a = resultSet.getInt(indexColumnA);
    String b = resultSet.getString("columnB");
    boolean c = resultSet.getBoolean("columnC");
    // ...
}

Některé typy JDBC ovladačů podporují i zpětný posun v tabulce (ResultSet.previous()), posun o libovolný počet řádků (ResultSet.relative(int)) nebo přístup k libovolnému řádku (ResultSet.absolute(int)).

Musíme však při vytváření instance Statement pomocí metody Connection.createStatement(int,%20int) jako první parametr předat ResultSet.TYPE_SCROLL_INSENSITIVE nebo ResultSet.TYPE_SCROLL_SENSITIVE (implicitně se použije ResultSet.TYPE_FORWARD_ONLY).

Předpřipravené dotazy

Pokud se v aplikaci některé SQL dotazy často opakují, je možné je předpřipravit a pak volat:

PreparedStatement insertStatement = conn.prepareStatement(
    "INSERT INTO myTable (a,b,c) VALUES (?,?,?);");
 
insertStatement.setInt(1,1);
insertStatement.setString(2,"Ahoj");
insertStatement.setBoolean(3,false);
insertStatement.execute();

Výhodou je jednak potenciální vyšší rychlost zpracování (záleží na konkrétním SŘBD a příslušném JDBC ovladači), ale také větší bezpečnost, neboť nemůže dojít k útoku typu SQL injection.

Dále zajistí správnou konverzi typů nezávisle na databázi, typicky při práci s java.sql.Timestamp nebo java.sql.Date.

Pozor! Pořadí parametrů se čísluje od jedné!

Získávání generovaných klíčů

Pokud máme v tabulce definovány automaticky generované primární klíče, můžeme někdy potřebovat získat hodnoty těchto klíčů pro vložená data. Standardní postup je tento:

st.execute(
    "INSERT INTO myTable (b,c) VALUES ('hello',false);",
    Statement.RETURN_GENERATED_KEYS);
 
ResultSet keys = st.getGeneratedKeys();

V proměnné keys bude uložena tabulka s vygenerovanými klíči. Počet řádků této tabulky bude odpovídat počtu vložených řádků (a tudíž počtu vygenerovaných klíčů) a počet sloupců bude odpovídat počtu složek primárního klíče. Pokud bude primární klíč jednoduchý (tj. nebude složený) a bude vložen pouze jeden záznam, tabluka keys bude obsahovat jeden řádek a jeden sloupec.

Bohužel ne všechny JDBC ovladače tento způsob získávání vygenerovaných klíčů podporují. Pokud ne, je nutné nahlédnout do dokumentace daného databázového serveru a zjistit, jestli existuje jiný způsob získání naposledy generovaných primárních klíčů. Většina serverů tuto informaci poskytuje prostřednictvím nějaké SQL funkce (např. MySQL poskytuje funkci LAST_INSERT_ID() a hsqldb funkci IDENTITY()).

Úprava dat pomocí ResultSet

Kromě klasického způsobu aktualizace dat prostřednictvím SQL příkazů některé typy JDBC ovladačů umožňují upravovat data prostřednictvím instance ResultSet. Tento způsob je velmi užitečný například při zobrazování a editaci dat pomocí tabulky (javax.swing.JTable).

Aby bylo možné jej použít, je nutné při vytváření instance instance ResultSet pomocí metody int) Connection.createStatement(int, int) jako druhý parametr předat ResultSet.CONCUR_UPDATABLE (implicitně se použije ResultSet.CONCUR_READ_ONLY).

Při úpravě aktuálního řádku (na něj jsme se dostali pomocí ResultSet.next(), ResultSet.previous(), ResultSet.relative(int) nebo ResultSet.absolute(int)) nejdříve voláme metody ResultSet.updateTyp(int, ?) nebo ResultSet.updateTyp(String, ?). Typ je typ daného atributu, prvním parametrem je pořadí nebo jméno atributu a druhým parametrem je jeho nová hodnota. Když úpravy dokončíme, změny se uloží do databáze pomocí metody ResultSet.updateRow().

Pokud chceme vložit nový řádek, zavoláme nejdříve metodu ResultSet.moveToInsertRow(), která nastaví kurzor na speciální řádek určený pro vkládání nových záznamů. Pak postupujeme stejně jako v předchozím případě, pouze na konci místo metody ResultSet.updateRow() zavoláme metodu ResultSet.insertRow(). Vrátit se na původní řádek, který byl aktuální před vložením nového, umožňuje metoda ResultSet.moveToCurrentRow().

Možnosti použití tohoto způsobu úpravy dat jsou obvykle omezené. Většinou není možné upravovat hodnoty atributů z připojených (JOIN) tabulek nebo z pohledů (VIEW). Také obvykle není možné tímto způsobem upravovat tabulky bez primárního klíče.

Základní zásady pro používání JDBC

Vždy nezapomeňte uvolnit všechny alokované prostředky a uzavřít otevřená spojení. A to i v případě výskytu výjimky.

Od Javy 7 je vhodné použít konstrukci try-with-resources, která automaticky uzavírá vytvořené objekty:

 
  try (Connection con = dataSource.getConnection();
       PreparedStatement st = con.prepareStatement("select * from books");
       ResultSet rs = st.executeQuery()) {
      List<Book> books = new ArrayList<>();
      while (rs.next()) {
          books.add(new Book(rs.getLong("id"), rs.getString("name")));
      }
      return books;
  } catch (SQLException e) {
      log.error("cannot select books", e);
  }
  return null;

Do Javy 6 bylo nutné použít konstrukci try {} finally {} a objekty ručně uzavírat.

Dále si dávejte pozor na bezpečné zpracování dat od uživatele, abyste se vyhnuli útokům typu SQL injection [1]. Nejlepší je pro vstup hodnot vždy používat předpřipravené dotazy. Pokud to není možné, například při uživatelem zadaném sloupci pro řazení v části ORDER BY, zkontrolujte alespoň vstup od uživatele regulárním výrazem, zda odpovídá očekávaným hodnotám.

DataSource

Nevýhodou výše popsaného způsobu připojení je nutnost znát parametry pro připojení k databázi, takže náš kód tyto údaje musí někde získat (např. z konfiguračního souboru). V prostředí aplikačních serverů a webových kontejnerů je vhodnější zodpovědnost za nastavení připojení k databázi delegovat na správce příslušného serveru nebo kontejneru. Aplikace potom s konfigurací pro připojení k databázo vůbec nemanipuluje a místo toho předpokládá, že jí příslušné spojení naváže a poskytne aplikační server.

Aplikace pak může získat připojení k databázi prostřednictvím tovární třídy implementující rozhraní javax.sql.DataSource. Instanci této třídy získáme obvykle pomocí dependency injection na místo označené anotací @Resource, tj.:

@Resource(name="jdbc/moje")
private DataSource source;
 
//...
 
// Spojení získáme pomocí metody getConnection()
Connection conn = source.getConnection();

přičemž dependency injection zajišťuje Java EE kontejner nebo Spring. Spojení je poté možné získat pomocí metody DataSource.getConnection().

Alternativou k dependency injection je přímé použití JNDI:

Context context = new InitialContext().lookup("java:comp/env");
DataSource source = (DataSource) context.lookup("jdbc/test");
 
// Spojení získáme pomocí metody getConnection()
Connection conn = source.getConnection();
 
// ...
 
// Zavoláním metody close() spojení vrátíme
conn.close();

Typ JDBC ovladače a URL pro spojení s databází označenou jdbc/test se v tomto případě nastavuje v konfiguraci serveru či kontejneru. Např. u webových aplikací pro servlet kontejner Tomcat se konfigurace provádí prostřednictvím souboru context.xml v adresáři META-INF, do nějž se pro příslušný kontext aplikace přidá řádek:

<Resource auth="Container" driverClassName="org.hsqldb.jdbcDriver" 
maxActive="100" maxIdle="30" maxWait="10000" name="jdbc/test" 
password="" type="javax.sql.DataSource" url="jdbc:hsqldb:mem:addressbook" 
username="sa"/>

Zejména v prostředí aplikačních serverů nebo webových kontejnerů se uplatňuje metoda recyklace spojení s databází (Connection pooling). Jde o to, že většinou má navázaní spojení nezanedbatelnou režii a také otevřené spojení spotřebovává systémové zdroje. Proto je součástí aplikačního serveru nebo kontejneru správce spojení, který otevře určitý počet spojení s databází a tyto otevřená spojení pak podle potřeby přiděluje běžícím procesům. Metoda DataSource.getConnection() nám potom nevytváří nové připojení k databázi, ale místo toho nám přidělí nějaké volné připojení, které je k dispozici.

Pooling databázových spojení

Je výkonově nevýhodné pro každý SQL příkaz navazovat nové spojení na databázi. Proto se používá pooling spojení, který otevřená spojení používá stále znovu.

Existuje několik běžně používaných implementací DataSource s poolingem spojení:

  • HikariCP
    • nejrychlejší implementace
  • Apache Commons DBCP
    • verze 1.3 pro Java <=5
    • verze 1.4 pro Java 6
    • verze 2.0 pro Java 7
  • Tomcat 7 poskytuje
    • DBCP 1.4 přebalenou z package org.apache.commons.dbcp do package org.apache.tomcat.dbcp.dbcp
  • Tomcat 8 poskytuje
    • DBCP 2.0 přebalenou z package org.apache.commons.dbcp2 do package org.apache.tomcat.dbcp.dbcp2
    • vlastní implementaci Tomcat JDBC v package org.apache.tomcat.jdbc.pool

Použití:

 
import com.zaxxer.hikari.HikariDataSource;
 
    public DataSource getDataSource() throws IOException {
        HikariDataSource ds = new HikariDataSource();
 
        //load connection properties from a file
        Properties p = new Properties();
        p.load(this.getResourceAsStream("/jdbc.properties"));
 
        //set connection
        ds.setDriverClassName(p.getProperty("jdbc.driver"));
        ds.setJdbcUrl(p.getProperty("jdbc.url"));
        ds.setUsername(p.getProperty("jdbc.user"));
        ds.setPassword(p.getProperty("jdbc.password"));
 
        return ds;
    }

kde soubor jdbc.properties obsahuje parametry připojení k databázovému serveru:

jdbc.driver=org.apache.derby.jdbc.ClientDriver
jdbc.url=jdbc:derby://localhost:1527/MojeDB
jdbc.user=nekdo
jdbc.password=heslo

nebo pro in-memory databázi

jdbc.driver=org.apache.derby.jdbc.EmbeddedDriver
jdbc.url=jdbc:derby:memory:MojeDB;create=true
jdbc.user=
jdbc.password=

Ve stand-alone aplikacích není nutné zadávat explicitně driver, ale uvnitř Tomcatu to nutné je, jeho classloadery znemožňují automatickou detekci driveru.

Databáze hsqldb

Hsqldb [2] je relační SQL databáze implementovaná v Javě. Obsahuje JDBC ovladač a je vhodná i pro vestavění přímo do aplikací. Nemusí se instalovat ani konfigurovat a podporuje také databáze uložené pouze v paměti. Je tak ideální pro experimentování s JDBC.

Třída JDBC driveru je org.hsqldb.jdbcDriver (ano, její název porušuje konvence pro pojmenování tříd).

URL pro přístup k databázi uložené v paměti je jdbc:hsqldb:mem:aname, kde aname je jméno databáze; jméno uživatele je sa a heslo je prázdné.

Spojení tedy získáme následujícím způsobem:

// Zavedeme JDBC driver
Class.forName("org.hsqldb.jdbcDriver");
// Vytvoříme spojení s databází
Connection conn = DriverManager.getConnection("jdbc:hsqldb:mem:test","sa","");

Odkazy a zdroje