Spring JDBC

Z FI WIKI
Verze z 28. 4. 2013, 14:22; 374351@muni.cz (diskuse | příspěvky)

(rozdíl) ← Starší verze | zobrazit aktuální verzi (rozdíl) | Novější verze → (rozdíl)
Přejít na: navigace, hledání

Motivace

Práce s čistým rozhraní JDBC je poměrně pracná. Je nutné stále dokola opakovat kusy kódu, které získávají spojení na databázi, formulují dotaz, zpracovávají odpověď a nakonec uklízí.

Například typická změna záznamu v tabulce vypadá nějak takto:

 
    public void updateBook(Book book) {
        Connection c = null;
        PreparedStatement st = null;
        try {
            c = dataSource.getConnection();
            st = c.prepareStatement("UPDATE BOOKS SET name=?,author=? WHERE id=?");
            st.setString(1, book.getName());
            st.setString(2, book.getAuthor());
            st.setInt(3, book.getId());
            st.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (st != null) st.close();
                if (c != null) c.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

Z toho celého jsou důležité vlastně jenom samotný SQL a nastavení jeho parametrů.

Existuje knihovna projektu Spring využívající návrhový vzor Template Method a nové rysy jazyka Java 5.0 k tomu, aby kód pro práci s databází mohl být maximálně jednoduchý. S její pomocí se dá výše uvedená metoda zapsat mnohem stručněji:

 
    public void updateBook(Book book) {
        jdbc.update("UPDATE BOOKS SET name=?,author=? WHERE id=?", book.getName(), book.getAuthor(), book.getId());
    }

To je rozdíl, který vidíte a cítíte :-)

Spring

Spring framework nabízí mnohem více než jen uvedenou knihovnu pro práci s JDBC, například Inversion-of-Control container,MVC webový framework, nástroje pro Aspect Oriented Programming a další. My však využijeme jen to nejnutnější.

Návod ke Spring JDBC je v jeho manuálu: Chapter 11. Data access using JDBC (ve verzi 3.1 Chapter 13: Data access with JDBC).

Spring potřebuje konfigurační soubor spring-context.xml, ve kterém lze nadefinovat implementaci DataSource a její připojení na databázi, např.:

 
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"
        >
 
    <context:component-scan base-package="cz.muni.fi.pa168.springjdbc"/>
    <context:annotation-config/>
    <tx:annotation-driven/>
 
    <!-- Derby alias JavaDB -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.apache.derby.jdbc.ClientDriver"/>
        <property name="url" value="jdbc:derby://localhost:1527/Books"/>
    </bean>
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
        <property name="transactionManager" ref="transactionManager"/>
    </bean>
 
</beans>

Spring pak automaticky zajistí, že každá třída kterou instanciuje  a která obsahuje následující metodu:

 
    @Resource
    public void setDataSource(DataSource dataSource) {
     //...
    }

dostane voláním této metody instanci DataSource. Kód aplikace se tak vůbec nestará o konfiguraci připojení k databázi, ponechává tuto práci Springu.

Příklad aplikace

Rozdíl mezi čistým JDBC a Spring JDBC si ukážeme na ukázkové aplikaci. Tu si stáhněte zde: spring-maven-app.zip.

Je to projekt pro Maven2, takže jej lze spustit z příkazového řádku, nebo otevřít v IntelliJ IDEA, nebo otevřít v NetBeans s pluginem pro Maven jako normální projekt.

Příklad obsahuje entitní třídu Book s položkami id, name a author

 
public class Book {
 
    private int id;
    private String name;
    private String author;
 
    public int getId() {
        return id;
    }
 
    public void setId(int id) {
        this.id = id;
    }
... atd. ...
}

a interface BooksManager, který slouží k základním CRUD (Create-Retrieve-Update-Delete) operacím s touto entitou.

 
public interface BooksManager {
 
    void addBook(Book book);
 
    List<Book> getAllBooks();
 
    Book getBookById(int id);
 
    List<Book> findBooksWithAuthor(String authorNameBeginsWith);
 
    void updateBook(Book book);
 
    void removeBook(int bookId);
 
    int getNumberOfBooks();
}

Rozhraní má dvě implementace. Obě jsou tzv. Spring-beans s injektovaným spojením na databázi.

Čisté JDBC

Implementace tohoto interface BooksManagerImplPureJDBC využívá jen čisté JDBC. Všimněte si, jak se stále dokola v každé metodě opakuje kód pro práci se spojením, statementy a resultsety:

 
public class BooksManagerImplPureJDBC implements BooksManager {
 
    private DataSource dataSource;
 
    @Resource
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
 
    public int getNumberOfBooks() {
        Connection c = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            c = dataSource.getConnection();
            st = c.prepareStatement("SELECT count(*) FROM BOOKS");
            rs = st.executeQuery();
            if (rs.next()) {
                return rs.getInt(1);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs != null) rs.close();
                if (st != null) st.close();
                if (c != null) c.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return 0;
    }
...
}

Spring JDBC

Naproti tomu implementace BooksManagerImplSpring využívá [Spring]] JDBC knihovnu a proto je kód mnohem stručnější:

 
public class BooksManagerImplSpring implements BooksManager {
 
    private JdbcTemplate jdbc;
 
    @Resource
    public void setDataSource(DataSource dataSource) {
        this.jdbc = new JdbcTemplate(dataSource);
    }
 
    public int getNumberOfBooks() {
        return jdbc.queryForInt("SELECT count(*) FROM BOOKS");
    }
 
    private static final RowMapper<Book> BOOK_MAPPER = new RowMapper<Book>() {
        public Book mapRow(ResultSet rs, int i) throws SQLException {
            return new Book(rs.getInt("id"), rs.getString("name"), rs.getString("author"));
        }
    };
 
    public List<Book> getAllBooks() {
        return jdbc.query("SELECT * FROM BOOKS", BOOK_MAPPER);
    }
 
    public Book getBookById(int id) {
        return jdbc.queryForObject("SELECT * FROM BOOKS WHERE id=?", BOOK_MAPPER, id);
    }
 
    public List<Book> findBooksWithAuthor(String authorNameBeginsWith) {
        //vsimnete si toho %, ktere v SQL prestavuje jakekoliv znaky
        return jdbc.query("SELECT * FROM BOOKS where author like ?", BOOK_MAPPER, authorNameBeginsWith + "%");
    }
 
    public void updateBook(Book book) {
        jdbc.update("UPDATE BOOKS SET name=?,author=? WHERE id=?", book.getName(), book.getAuthor(), book.getId());
    }
 
    public void removeBook(int bookId) {
        jdbc.update("DELETE FROM BOOKS WHERE id=?", bookId);
    }
...
}

Využívá zejména třídu org.springframework.jdbc.core.JdbcTemplate, která poskytuje mnoho výhodných metod.

INSERT, UPDATE, DELETE

Pokud potřebujeme dělat SQL operace bez návratových dat, tj. UPDATE, INSERT nebo DELETE, stačí jediný příkaz udávající SQL s otazníky na místě, kam se mají nahradit parametry, následovaný hodnotami parametrů ve správném pořadí.

 
  jdbc.update("INSERT INTO BOOKS (id,name,author) VALUES (?,?,?)", book.getId(), book.getName(), book.getAuthor());
 
  jdbc.update("UPDATE BOOKS SET name=?,author=? WHERE id=?", book.getName(), book.getAuthor(), book.getId());
 
  jdbc.update("DELETE FROM BOOKS WHERE id=?", bookId);

Metoda update() využívá proměnného počtu parametrů, což je rys přidaný do jazyka Java od verze 5. Pokud SQL příkaz nepotřebuje parametry, stačí žádné neuvést.

SELECT 

Příkaz SELECT vrací data, která je třeba zpracovat. Spring toto řeší pomocí callbackové třídy RowMapper, od které můžeme výhodně vytvářet anonymní potomky implementující metodu mapRow(), jenž převádí jeden řádek ResultSetu na požadovaný objekt. V našem příkladě to je na instanci třídy Book:

 
    private static final RowMapper<Book> BOOK_MAPPER = new RowMapper<Book>() {
        public Book mapRow(ResultSet rs, int i) throws SQLException {
            return new Book(rs.getInt("id"), rs.getString("name"), rs.getString("author"));
        }
    };

Samotný SELECT pak zabere opět jen jeden příkaz. Podle toho zda čekáme ve výsledku jeden objekt, nebo celý seznam, nebo číslo, použijeme metodu queryForObject(), query() nebo queryForInt():

 
  Book b = simple.queryForObject("SELECT * FROM BOOKS WHERE id=?", BOOK_MAPPER, id);
 
  List<Book> books = simple.query("SELECT * FROM BOOKS", BOOK_MAPPER);
 
  int numBooks = simple.queryForInt("SELECT count(*) FROM BOOKS");

Generované klíče

Toto je relativně komplikované téma.

Když ukládáme entitní objekt poprvé do databáze, je třeba mu přiřadit nějaké unikátní id. Bohužel, v různých databázích se to řeší různě, neexistuje univerzální řešení, a ani Spring nám v tom nepomůže úplně.

V zásadě jsou dvě skupiny databází. Jedna skupina, do které patří např. PostgreSQL nebo Oracle, podporuje tzv. sekvence, což jsou objekty, které při každém dotazu na svoji hodnotu vrací jinou hodnotu, obvykle o jedna vyšší než byla ta předchozí. Lze u nich vkládat nadvakrát, tedy nejdřív získat SELECTem novou hodnotu id ze sekvence a pak tuto hodnotu nastavit INSERTem společně s ostatními hodnotami. Nebo lze nastavit, že default hodnota sloupce id se bere ze sekvence a jedním INSERTem vládat jen ostatní sloupce.

Druhá skupina databází, kam patři např. JavaDB (Derby) nabo MySQL, sekvence nepodporuje, ale podporuje typ sloupce, který si sám přiřazuje unikátní hodnotu, a tuto hodnotu dokážou vrátit.

JDBC ovladače databází mohou pomocí metody Statement.getGeneratedKeys() u obou skupin databází vracet vygenerovanou hodnotu id. Třeba ovladače Oracle, JavaDB, MySQL s PostgreSQL od verze 8.4 to dělají, ovladače PostgreSQL až do verze 8.3 včetně to nedělá.

O tom, zda ovladač dokáže vracet generované klíče, by měl informovat v tzv. metadatech, která lze číst takto:

 
boolean supportsGetGeneratedKeys = dataSource.getConnection().getMetaData().supportsGetGeneratedKeys();

Bohužel, v případě JavaDB, která klíče vracet umí, hlásí metadata, že je vracet neumí :-(

Takže buď můžeme při psaní kódu rezignovat na přenos mezi databázemi, a psát kód pro konkrétní databázi, nebo musíme v kódu dělat výhybky podle toho, co je to za databázi.

V ukázkovém příkladu je zvolena možnost výhybek, nejdřív je detekována podpora klíčů a jméno databáze:

 
    Connection c = dataSource.getConnection();
    this.supportsGetGeneratedKeys = c.getMetaData().supportsGetGeneratedKeys();
    this.databaseProductName = c.getMetaData().getDatabaseProductName();
    c.close();

a při samotném vkládání jsou rozvedeny tři možnosti.

  • pro databáze podporující vracení generovaných klíčů a hlásící tuto skutečnost korektně v metadatech (Oracle, MySQL, PostgreSQL 8.4) je využita třída Springu SimpleJdbcInsert jejíž použití je, troufám si říci, opravdu simple
  • u JavaDB alias Derby je nutné připravit PreparedStatement ručně, protože z metadat Spring získá mylnou informaci
  • u PostgreSQL 8.3 lze využít netradiční slovo RETURNING pro vrácení vygenerovaného klíče
 
    public void addBook(final Book book) {
        if (supportsGetGeneratedKeys) {
            // u databazi s korektnim JDBC3 ovladacem (MySQL,Oracle,PostgreSQL 8.4) staci jeden radek
            book.setId(insertBook.executeAndReturnKey(new BeanPropertySqlParameterSource(book)).intValue());
        } else if (databaseProductName.equals("Apache Derby")) {
            // Derby umi vracet klice, ale metadata to nehlasi
            KeyHolder keyHolder = new GeneratedKeyHolder();
            jdbc.update(
                    new PreparedStatementCreator() {
                        public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
                            PreparedStatement ps =
                                    connection.prepareStatement("INSERT INTO BOOKS (name,author) VALUES (?,?)", new String[]{"id"});
                            ps.setString(1, book.getName());
                            ps.setString(2, book.getAuthor());
                            return ps;
                        }
                    },
                    keyHolder);
            book.setId(keyHolder.getKey().intValue());
        } else if (databaseProductName.equals("PostgreSQL")) {
            // PostgreSQL od verze 8.3 ma nestandardni klicove slovo RETURNING
            // PostgreSQL od verze 8.4 umi getGeneratedKeys
            int id = jdbc.queryForInt("INSERT INTO BOOKS (name,author) VALUES (?,?) RETURNING id", book.getName(), book.getAuthor());
            book.setId(id);
        } else {
            // v ostatnich pripadech tezko rict
            throw new RuntimeException("nepodporuje getGeneratedKeys() a neni to Postgres ani Derby");
        }
    }

Spring IoC a spojení na databázi

Ukázková aplikace má v konfiguračním souboru pro Maven2 pom.xml uvedeny závislosti na JDBC ovladačích pro JavaDB, PostgreSQL a MySQL, je tedy schopna pracovat se všemi z nich.

Zároven aplikace vyžívá tzv. Spring IoC (Inversion-of-Control) container, který je inicializován v metodě Main.main() ze souboru spring-context.xml. Tento IoC container způsobuje, že třídy označené anotací @Repository (což jsou třídy BooksManagerImplSpring a BooksManagerImplPureJDBC) budou automaticky nainstancovány, a budou zavolány jejich metody označené anotací @Resource, což jsou metody

 
    @Resource
    public void setDataSource(DataSource dataSource) {
     //...
    }

a jako argument dostanou DataSource definovaný v souboru spring-context.xml. V něm jsou opět připraveny verze pro JavaDB, PostgreSQL a MySQL, stačí tu vybrat správnou, a té nastavit JDBC URL, username a password.

Ukázková aplikace se snaží mít strukturu podobnou reálné aplikaci. Díky použití Spring IoC containeru ji můžete dokonce téměř beze změny použít i jako základ webové aplikace, jen stačí vyměnit ve spring-context.xml implementaci DataSource na jinou, v případě TomCatu např. stačí nahradit původní

 
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.apache.derby.jdbc.ClientDriver"/>
        <property name="url" value="jdbc:derby://localhost:1527/Books"/>
    </bean
 

za odkaz na JNDI pool databázových spojení:

 
    <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean" scope="singleton">
        <property name="jndiName" value="/jdbc/books"/>
        <property name="resourceRef" value="true"/>
        <property name="proxyInterface" value="javax.sql.DataSource" />
    </bean>

Transakce

Někdy je potřeba zajistit, že několik SQL příkazů se buď provede jako celek, nebo se neprovede vůbec. Klasickým příkladem je přepis peněz v bance z účtu na účet - buď musí peníze z jednoho účtu ubýt a na druhém přibýt, nebo se nesmí stát nic. Nelze připustit stav, kdy peníze z jednoho účtu odejdou, ale nikam se nepřipíšou, protože třeba zrovna vypnuli přívod elektřiny k bankovnímu počítači.

Řešením jsou transakce. Transakce se zahájí, SQL příkazy se dělají a pokud všechny skončí úspěšně, provede se commit, který všechny změny udělá najednou. Pokud ne, provede se rollback a jako kdyby se nic nestalo.

V čistém JDBC se transakce zahajují voláním metody setAutoCommit(false) a končí voláním commit() případně rollback() a opětovným nastavením setAutoCommit(true).

Ve Springu je to mnohem jednodušší. Stačí příslušnou metodu označit anotací:

 
    @Transactional
    public void slozitaOperace() {
           simple.update("...");
           simple.update("...");
           simple.update("...");
           //...
    }

Aby anotace zafungovala, je potřeba mít ve spring-context.xml nastavený transakční manager se jménem transactionManager a aktivované používání anotací, což v ukázkové aplikaci je:

 
    <tx:annotation-driven/>
 
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
        <property name="transactionManager" ref="transactionManager"/>
    </bean>

Podrobnosti jsou popsány v Spring Framework: Chapter 9. Transaction management.

Pokud chcete vidět, kdy jsou transakce prováděny, přidjte do log4.xml toto:

 
    <category name="org.springframework.transaction">
        <priority value="debug"/>
    </category>
    <category name="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <priority value="debug"/>
    </category>

a pokud chcete vidět i SQL příkazy, přidejte i

 
    <category name="org.springframework.jdbc.core.JdbcTemplate">
        <priority value="debug"/>
    </category>