pátek 13. ledna 2006

Návrhový vzor Template method a jeho aplikace v prostředí JDBC

Tento článek by mohl klidně nést podtitul Efektivní práce s databází v Jave: stop nadbytečnému kódu. Hodně často se diskutuje práce s JDBC, což je javovské rozhraní pro práci s databází. Bohužel už méně často se diskutuje o tom jak psát JDBC kód efektivně. V tomto článku si ukážeme jak na to za pomoci návrhového vzoru Template method.

Přistupovat k databázi v Jave není vůbec složité a vše potřebné je definováno rozhraním JDBC (Java Database Connectivity). Práce s tímto rozhraním se skládá z několika kroků.

  • získání databázového připojení
  • vytvoření statementu
  • nastavení parametru a exekuce SQL
  • procházení resultsetu a zpracování dat
  • uzavření resultsetu, statementu a datab. připojení

Typický kód pak vypadá následovně

Connection con = null;
PreparedStatement ps =
null;
ResultSet rs =
null;
try{
   
con = getConnection();
    ps = con.prepareStatement
("select * from foo");
    rs = ps.executeQuery
();
   
while(rs.next()){
      
//zpracuj hodnoty
   
}
}
finally{
   
rs.close();
    ps.close
();
    con.close
();
}

Všechny tyto kroky, kromě zpracování vrácených dat, se neustále v kódu opakují pro každý SQL dotaz. Tím pádem nám vzniká zbytečně velké množství redundantního kódu. Navíc se musíme srovnat s všudypřítomnou SQLException, která je nejen příliš obecná, ale navíc i kontrolovaná (checked). Protože se jedná o kód redundantní, máme tendenci používat osvědčenou metodu "najdi a vlep".

Tím pádem se muže lehce přihodit, že do kódu zavlečeme chybu, která se bude posléze velice těžko dohledávat. Takovým klasickým případem je nezavření databázového připojení resp. jeho vrácení do poolu. Navíc, kdo by si nechtěl ušetřit práci s neustálého kopírování kódu že? Řešení těchto problému (redundance, exception handling, odpovědnost) spočívá v separování společného kódu ala návrhový vzor Template Method.

Template method

Template method je návrhový vzor spadající do rodiny vzoru chování (bahavioral). Smysl tohoto vzoru je založen na tom, že na abstraktnější úrovni předka je vytvořen scénář (metoda), který je složen z volání několika polymorfních metod. Potomkové používají scénář tak, že polymorfní operace přepíší a scénář zavolají.

Diagram tříd (UML)

Kostra scénáře je daná metodou templateMethod, ze které se volají kromě jiného metody operation1 a operation2. Ty jsou v předkovi nadefinovány jako abstraktní, takže je možná jejich prázdná implementace. Potomek tyto metody implementuje a tím vyplní zbývající části scénáře. Vlastní spuštění se provede voláním metody new ConcreteClass().templateMethod();.

Při aplikaci tohoto vzoru na náš problém, bude společný kód, v podobě získání databázového připojení, vytvoření statementu, nastavení parametru, exekuce SQL, procházení resultsetu a uzavření všeho potřebného, izolovaný v předkovi. Ten bude definovat dvě abstraktní metody. Metodu pro získání databázového připojení a metodu pro zpracování řádky resultsetu.

Diagram tříd (UML)

Popis diagramu

Odkazy vedou do vygenerovaného javadocu, který je navázán na zdrojový kód. Tam je i podrobný popis rozhraní jednotlivých metod

Objekt JDBCTemplate definuje dvě šablonové metody určené k volání a to executeQuery a executeUpdate. Dále definuje dvě polymorfní metody a to getConnection a handleRow. Potomkové mohou přepsat ješte metody setParams a metodu handleSQLException. V diagramu tříd je potomek ConcreteJDBCTemplate, který realizuje získání databázového připojení a implementuje handleRow prázdným tělem. Od něj je vhodné děděním vytvářet další specializované třídy např. je možné vytvořit třídu, která bude výsledek dotazu vracet jako seznam, kde jednotlivé řádky budou realizovány jako mapa (klíč jméno sloupečku).

Zdrojový kód JDBCTemplate

package cz.sweb.pichlik.jdbctemplate;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;

/**
* Abstraktní předek pro JDBC operace (podporuje
<tt>select, insert, update a delete</tt>), který
* umožňuje efektivně pracovat resultset. Díky tomu se nekontaminuje klientský kód
* často se opakujícím JDBC kódem a nemusí se explicitně starat o zpracování
*
<code>{@link java.sql.SQLException}</code>.
*
<p>
* Potomkové musí přepsat metodu <code>{@link #handleRow(ResultSet)}</code> pokud
* chtějí zpracovat resultset (platí pro
<tt>select</tt>) a <code>{@link #getConnection()}</code>,
* kterou budou poskytovat databázové připojení.
</p>

*
@author    Roman "Dagi" Pichlík
*/
public abstract class JDBCTemplate {
 
 
/**
   * Konstanta označující SQL příkaz
<tt>select</tt>
  
*/
 
private static final int SELECT_QUERY = 0;
 
 
/**
   * Konstanta označující
<tt>insert, update, delete</tt>
  
*/
 
private static final int UPDATE_QUERY = 1;
 
   
 
/**
   * Provede
<tt>select</tt> SQL příkaz. Přes výsledný resultset se prochází a každý
   * řádek se předává k zpracování metodě
<code>{@link #handleRow(ResultSet)}</code>.
   * V případě, že vznikne
{@link SQLException} je předána k zpracování metodě
   *
<code>{@link #handleSQLException(SQLException, String, Object[])}</code>
  
*
   *
@param sql SQL příkaz (<em>nesmí být <code>null</code></em>)
   *
@param params parametry pro SQL (<em>může být <code>null</code></em>)
   *
   *
@throws NullPointerException pokud je SQL <code>null</code>
  
*
   *
@see #setParams(PreparedStatement, Object[])
   */
 
public final void executeQuery(String sql, Object params[]) {
   
try{
     
executeQueryInternal(sql, params, SELECT_QUERY);
   
}catch(SQLException e){
     
throw handleSQLException(e, sql, params);
   
}
  }
 
 
/**
   * Provede SQL příkaz typu
<tt>insert, update, delete</tt> a vrátí počet
   * modifikovaných řádek. V případě, že vznikne
{@link SQLException} je předána k zpracování metodě
   *
<code>{@link #handleSQLException(SQLException, String, Object[])}</code>.
   *
   *
@param sql SQL příkaz (<em>nesmí být <code>null</code></em>)
   *
@param params parametry pro SQL (<em>může být <code>null</code></em>)
   *
   *
@return počet modifikovaných řádek
   *
   *
@throws NullPointerException pokud je SQL <code>null</code>
  
*/
 
public final int executeUpdate(String sql, Object params[]) {
   
try{
     
return executeQueryInternal(sql, params, UPDATE_QUERY);     
   
}catch(SQLException e){
     
throw handleSQLException(e, sql, params);
   
}
  }

 
/**
   * Realizuje vlastní vykonání příkazu
   *
@param sql SQL příkaz
   *
@param params parametr
   *
@param queryType typ SQL  (insert, select, update ,delete)
   *
   *
@return rows affected
   *
   *
@throws SQLException v případě, že nějaká vznikne
   */
 
private int executeQueryInternal(String sql, Object params[], int queryType) throws SQLException{
   
if(sql == null){
     
throw new NullPointerException();
   
}
   
int rowsAffected = -1;
    Connection con =
null;
    PreparedStatement ps =
null;
    ResultSet rs =
null;
   
try{
     
con = getConnection();
      ps = con.prepareStatement
(sql);
     
if(params != null){
       
setParams(ps, params);
     
}
     
     
if(queryType == SELECT_QUERY){
       
rs = ps.executeQuery();
       
while(rs.next()){       
         
handleRow(rs);
       
}
      }
else{
       
rowsAffected = ps.executeUpdate();
     
}
    }
finally{
     
close(con, ps, rs);     
   
}
   
return rowsAffected;
 
}
 
 
/**
   * Vrací databázové připojení, které se použije pro vykonání SQL příkazu.  
   *
   *
@return databázové připojení
   */
 
protected abstract Connection getConnection() throws SQLException;
 
 
/**
   * Metoda, ve které se implementuje vlastní zpracování řádku resultsetu.
   * 
   *
@param rs resultset
   *
@throws SQLException v případě, že nějaká vznikne
   */
 
protected abstract void handleRow(ResultSet rs) throws SQLException;
 
 
/**
   * Slouží k nastavení parametrů pro SQL příkaz podle pořadí. Nastavení hodnot
   *
<code>null</code> není podporováno.
   *
   *
<h4><a name="supported-types">Podporované datové typy</a></h4>
  
* <ul>
  
<li><code>{@link BigDecimal}</code></li>
  
<li><code>{@link Date}</code></li>
  
<li><code>{@link String}</code></li>
  
<li><code>{@link InputStream}</code></li>
  
<li><code>{@link Timestamp}</code></li>
  
<li><code>{@link java.lang.Double}</code></li>
  
<li><code>{@link java.lang.Float}</code></li>
  
<li><code>{@link Integer}</code></li>
  
<li><code>{@link Long}</code></li>
  
* </ul>
  
*
   *
@param ps statement
   *
@param params parametry
   *
@throws SQLException v případě, že nějaká vznikne
   *
@throws UnsupportedOperationException pokud se jedná o parametr mimo <a href="#supported-types">podporované
   * datové typy
</a>.
   */
 
protected void setParams(PreparedStatement ps, Object params[]) throws SQLException{
   
Object paramValue = null;
   
for (int i = 0; i < params.length;) {
     
paramValue = params[i];
     
if(paramValue == null){
       
throw new UnsupportedOperationException("Nastavení hodnoty null není podporováno");
     
} else if(paramValue instanceof BigDecimal){
         
ps.setBigDecimal(++i, (BigDecimal)paramValue);
     
}else if (paramValue instanceof Date){
       
ps.setDate(++i,(Date)paramValue);
     
}else if (paramValue instanceof InputStream){
       
try{
         
InputStream is = (InputStream) paramValue;
          ps.setBinaryStream
(++i,is, is.available());
       
}catch(IOException e){
         
throw new RuntimeException(e);
       
}
      }
else if (paramValue instanceof String){
         
ps.setString(++i, (String) paramValue);
     
}else if (paramValue instanceof Timestamp){
       
ps.setTimestamp(++i, (Timestamp)paramValue);
     
}else if (paramValue instanceof Double){
       
ps.setDouble(++i, ((Double)paramValue).doubleValue());
     
}else if (paramValue instanceof Float){
       
ps.setFloat(++i, ((Float)paramValue).floatValue());
     
}else if (paramValue instanceof Integer){
       
ps.setInt(++i, ((Integer)paramValue).intValue());
     
}else if (paramValue instanceof Long){
         
ps.setLong(++i, ((Long)paramValue).longValue());
     
}else {          
         
throw new UnsupportedOperationException("Podpora datového typu " + paramValue.getClass() + " není prozatím implementována!");
     
}
        }
  }
 
 
/**
   * Umožňuje ošetřit vzniklou
<code>{@link SQLException}</code>. Defaultně je
   * implementována tak, že vzniklá výjimka je obalena do
<code>{@link DatabaseException}</code>.
   * Potomkové mohou toto chování libovolně přepsat např. výjimku zapsat do logu,
   * lokalizovat text výjímky apod.
   *
   *
@param e vzniklá výjimka
   *
@param sql SQL příkaz, při kterém vznikla výjímka
   *
@param params parametry pro SQL
   */
 
protected DatabaseException handleSQLException(SQLException e, String sql, Object params[]){
   
String message = "Vznikla neočekávaná chyba během databázové operace!";
   
return new DatabaseException(message, e, sql, params);
 
}
 
 
/**
   * Provede bezpečné uzavření statemntu, resultsetu a databázového připojení.
   *
@param con databázové připojení
   *
@param ps statemnt
   *
@param rs resultset
   *
@throws SQLException v případě, že nějaká vznikne
   */
 
private void close(Connection con, PreparedStatement ps, ResultSet rs) throws SQLException {
   
try{
     
if(rs != null){
       
rs.close();
     
}
    }
finally{
     
try{
       
if(ps != null){
         
ps.close();
       
}
      }
finally{
       
if(con != null){
         
con.close();
       
}
      }
    }
  }
}

Ukázka použití

Jak jsem zmiňoval dříve, objekt ConcreteJDBCTemplate je vhodný kandidát na rozšíření. Pro účely ukázky je implementován tak, že s každým požadavkem na databázové připojení vytvoří vždy nové na základě systémových properties. Při reálném nasazení by vhodné databázové připojení poolovat. Následuje kód z objektu ExampleTemplate. Za povšimnutí stoji dědění ConcreteJDBCTemplate a to pomocí anonymní vnitřní třídy.

  /**
   * Smazání řádků z tabulky  
   */
 
public void delete(){
   
String deleteQuery = "delete from test";
   
int rowsAffected =
     
new ConcreteJDBCTemplate().executeUpdate(deleteQuery, null);
    System.out.println
("Rows deleted:" + rowsAffected);
 
}
 
 
/**
   * Vložení řádků o tabulky
   */
 
public void insert(){
   
Object params[] = {new Integer(1), new Integer(1), new Double(0.1), new Timestamp(new Date().getTime()), "Foo"};
    String insertQuery =
"insert into test (id, _integer, _double, _timestamp, _string) values (?, ?, ?, ?, ?)";
   
int rowsAffected =
     
new ConcreteJDBCTemplate().executeUpdate(insertQuery, params);
    System.out.println
("Rows inserted:" + rowsAffected);
 
}
 
 
/**
   * Update řádků z tabulky  
   */
 
public void update(){
   
String updateQuery = "update test set _integer = ?, _double = ?, _timestamp = ?, _string = ?";
    Object params
[] = {new Integer(2), new Double(0.2), new Timestamp(new Date().getTime()), "Hoo"};
   
int rowsAffected =
     
new ConcreteJDBCTemplate().executeUpdate(updateQuery, params);
    System.out.println
("Rows updated:" + rowsAffected);
 
}
 
 
/**
   * Vypsání výsledků SQL příkazu na konzoli  
   */
 
public void select(){
   
String sql = "select * from test";
   
new ConcreteJDBCTemplate(){
     
protected void handleRow(ResultSet rs) throws SQLException {       
       
Integer id = Integer.valueOf(rs.getInt("id"));
        Integer _integer = Integer.valueOf
(rs.getInt("_integer"));
        Double _double = Double.valueOf
(rs.getDouble("_double"));
        Timestamp _timestamp = rs.getTimestamp
("_timestamp");
        String _string = rs.getString
("_string");
        ...
     
}
    }
.executeQuery(sql, null)
 
}

Ukázkové příklady operují nad databází, která je vytvořena přiloženým SQL skriptem (syntaxe MySQL). V případě, že chcete příklady spustit, musí být vytvořena tabulka test. K spuštění slouží předpřipravená dávka, kterou je potřeba upravit podle lokálního prostředí a použitého JDBC driveru (ten si musíte stáhnout podle databáze). K dispozici jsou samozřejmě i zdorojové kódy inkriminovaných tříd.

Použíté zdroje

Pro převod zdrojových kódů byl použita online verze open source programu Java2Html (Open source Java (and others) to (X)HTML (and TeX and RTF) converter).