Detta inlägg ingår i serien Spring från början och kommer att behandla hur man kan jobba med JdbcTemplate och andra centrala klasser i modulen spring-jdbc.

Templating

Ett designmönster som används flitigt i Spring är ”template method”. En template-metod definierar ett skelett för en algoritm. Algoritmen är abstrakt och det är upp till subklasser att definiera det konkreta beteendet för algoritmen. Ett annat designmönster som påminner mycket om ”template method” är mönstret Strategy, en av skillnaderna mellan ”template method” och ”Strategy” är att ”template method” använder arv medan Strategy-mönstret använder delegat för att uppnå sitt mål. Nedanstående visar användningen av Strategy-mönstret via ett exempel som är kraftigt influerat från ett wikipedia-exempel som använder template method.

/** A game that can be played... */
public interface Game {
    void initializeGame();
    void makePlay(int player);
    boolean endOfGame();
    void printWinner();
}
/** Concrete implementation of the chess game... */
public class Chess implements Game {
    public void initializeGame() {
        // do chess stuff...
    }

    public void makePlay(int player) {
        // do chess stuff...
    }

    public boolean endOfGame() {
        // do chess stuff...
        return false;
    }

    public void printWinner() {
        // do chess stuff...
    }
}
/** Concrete implementation of the monopoly game... */
public class Monopoly implements Game {
    public void initializeGame() {
        // do monopoly stuff...
    }

    public void makePlay(int player) {
        // do monopoly stuff...
    }

    public boolean endOfGame() {
        // do monopoly stuff...
        return false;
    }

    public void printWinner() {
        // do monopoly stuff...
    }
}
/** The class containing the template method. */
public class GameTemplate {
    /**
     * This is a template method that plays the provided
     * game using a specified algorithm (initialize,
     * play, end, print winner)
     *
     * The template method is common to several games in
     * which players play against the others, but only one
     * player is playing at a given time.
     */
    public void play(int playersCount, Game game) {
        game.initializeGame();

        int j = 0;
        while (!game.endOfGame()){
            game.makePlay(j);
            j = (j + 1) % playersCount;
        }

        game.printWinner();
    }
}

Exemplet ovan visar hur en algoritm appliceras på ett Game-objekt i metoden play. Exemplet är en aning förändrat jämfört med originalet så till vida att klassen GameTemplate inte är ett Game utan endast innehåller själva algoritmen för att få en lösare koppling mellan algoritmen och de faktiska Game-objekten.

Nu när vi känner till designmönstret ”template method” kan vi kika vidare på templaten som tillhandahålls av Spring för JDBC-relaterade operationer, nämligen JdbcTemplate.

JdbcTemplate

Klassen JdbcTemplate är kärnan i den modul som kallas spring-jdbc. Klasserna i denna modul används när access till databasen sker via JDBC snarare än via ett O/R-mappningsramverk såsom Hibernate.

Att använda rå JDBC kan medföra en hel del problem för utvecklaren där Connections, Statements, ResultSets måste stängas, exceptions måste fångas osv. Kod som är skriven för rå JDBC-användning är ofta väldigt utförlig (verbose) och leder lätt till fel (såsom att glömma att stänga ett statement). Springs JdbcTemplate hjälper utvecklaren med många delar av de uppgifter som krävs för att använda JDBC.

TaskSpringYou
Connection ManagementX
SQLX
Statement ManagementX
ResultSet ManagementX
Row Data RetrievalX
Parameter DeclarationX
Parameter SettingX
Transaction ManagementX
Tabellen ovan visar de delar som `JdbcTemplate` hjälper dig med jämfört med att använda rå JDBC.

Nedanstående exempel visar vanlig användning av rå JDBC med en enkel parameteriserad SELECT och en mappning mellan ett Person-objekt och ett ResultSet.

private static final String PERSON_QUERY =
        "select first_name, last_name, phone_number "
        + "from person where last_name = ?";

public List getPersonList1() {
    List list = new ArrayList();
    Connection connection = null;
    PreparedStatement statement = null;
    try {
        connection = getConnection();
        statement = connection.prepareStatement(PERSON_QUERY);
        statement.setString(1, "Svensson");
        ResultSet rs = statement.executeQuery();
        while (rs.next()) {
            Person person = new Person(
                rs.getString("first_name"),
                rs.getString("last_name"),
                rs.getString("phone_number"));
                list.add(person);
        }
        rs.close();
    } catch (SQLException e) {
        // handle exception somehow...
    } finally {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                // handle exception somehow...
            }
        }

        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                // handle exception somehow...
            }
        }
    }

    return list;
}

Exemplet kan förenklas avsevärt genom användning av JdbcTemplate vilket metoden getPersonList2 i nedanstående exempel visar.

public List getPersonList2 () {
    RowMapper personMapper = new RowMapper() {
        // Maps a row to a Person
        public Object mapRow(
                ResultSet rs,
                int rowNum) throws SQLException {

            return new Person(
                rs.getString("first_name"),
                rs.getString("last_name"),
                rs.getString("phone_number"));
        }
    };

    return getJdbcTemplate().query(
        PERSON_QUERY, new Object[] {"Svensson"}, personMapper);
}

Vi antar i exemplet ovan att man kan få tag på en JdbcTemplate via metoden getJdbcTemplate().

Några saker värda att notera:

  • Kod för att hantera connections, statements etc. saknas helt. Detta hanteras av JdbcTemplate som öppnar och stänger alla resurser.
  • Mappning mellan SQL-resultatet och affärsobjektet sker i en RowMapper. Detta är ett tydligt mönster och RowMapper-objektet kan återanvändas om det deklareras utanför metoden. Förutom RowMapper finns även andra sätt att mappa data och dessa kan återfinnas i Springdokumentationen.
  • Ingen felhantering krävs. Alla eventuella exceptions fångas av JdbcTemplate och konverteras sedan till Springs egna unchecked exceptions mha. SQLExceptionTranslator. Istället för att kasta ett checked exception med en kryptisk databasunik felkod kan tydliga exceptions såsom BadSqlGrammarException eller OptimisticLockingFailureException kastas. De databaser som supportas out-of-the-box är DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL och Sybase. Naturligtvis går det även att plugga in egna SQLExceptionTranslators.

Det finns även några inbyggda RowMappers. Metoden queryForInt som exemplifieras nedan skapar en intern RowMapper där ett SQL-resultat mappas mot ett heltal.

public int countPeople() {
    return getJdbcTemplate().queryForInt(
            "select count(*) from person ");
}

I exemplen ovan har endast SELECT-frågor mot databasen ställts. Det finns även metoder för att utföra uppdateringar, se exemplet nedan för en enkel parameteriserad uppdatering:

private static final String UPDATE_PERSON =
    "update person set phone_number = ?"
    + "where first_name = ? and last_name = ?";

public void updatePhoneNumber(
        String firstName, String lastName, String phoneNumber) {

    getJdbcTemplate().update(
            UPDATE_PERSON,
            new Object[] {phoneNumber, firstName, lastName});
}

JdbcTemplate behöver endast konfigureras upp med en datakälla. Det finns några bekväma klasser att ärva från för att enkelt få tag på JdbcTemplate-objektet. Klassen JdbcDaoSupport kan användas för DAO-objekt medan klassen AbstractTransactionalDataSourceSpringContextTests fungerar bra för JUnit-tester. Nedanstående exempel visar detta.

Så här kan en DAO-klass se ut:

public class MyDao extends JdbcDaoSupport {
    private static final String UPDATE_PERSON =
        "update person set phone_number = ? "
        + "where first_name = ? and last_name = ?";

    public void updatePhoneNumber (
            String firstName, String lastName, String phoneNumber) {

        getJdbcTemplate().update(
                UPDATE_PERSON,
                new Object[] {phoneNumber, firstName, lastName});
    }
}

När en DAO konfigureras så måste en DataSource injectas, detta kan exempelvis ske via en JNDI-uppslagning:

<bean
  id="myDataSource"
  class="org.springframework.jndi.JndiObjectFactoryBean">

    <property name="lookupOnStartup" value="false" />
    <property name="proxyInterface" value="javax.sql.DataSource" />
    <property name="resourceRef" value="true" />
    <property name="jndiName" value="jdbc/myds" />
</bean>

Själva DAO-objekten kan exempelvis deklareras i en egen fil som kan användas både för produktion och för test.

<bean id="myDao" class="se.cygni.sample.spring.template.jdbc.MyDao">
    <property name="dataSource" ref="dataSource" />
</bean>

När applikationen ska konfigureras för test så kan en DataSource konfigureras utan JNDI-uppslagning:

<bean
  id="myDataSource"
  class="org.springframework.jdbc.datasource.DriverManagerDataSource" >

    <property name="driverClassName"
        value="oracle.jdbc.driver.OracleDriver" />
    <property name="url" value="jdbc:oracle:thin:@host:port:schema" />
    <property name="username" value="scott" />
    <property name="password" value="tiger" />
</bean>

Ett JUnit-test kan se ut enligt följande:

public class MyDaoTest
        extends AbstractTransactionalDataSourceSpringContextTests {

    private MyDao dao;

    protected String[] getConfigLocations() {
        return new String[] {
                "testDataSourceContext.xml",
                "daoContext.xml"
        };
    }

    public void testDao() {
        dao.updatePhoneNumber("Johnny", "Puma", "555-555 5555");

        int count = getJdbcTemplate().queryForInt(
            "select count(*) from person "
            + "where first_name = 'Johnny' "
            + "and last_name = 'Puma' "
            + "and phone_number = '555-555 5555'");

        assertTrue(count == 1);
    }

    public void setMyDao(MyDao dao) {
        this.dao = dao;
    }
}

I testet ovan visas hur dels MyDao injectas i testklassen eftersom filen daoContext.xml anges som en config-location. Även filen testDataSourceContext.xml inkluderas vilket leder till att en DataSource finns tillgänglig och då injectas denna DataSource i basklassen AbstractTransactionalDataSourceSpringContextTests vilket leder till att metoden getJdbcTemplate blir tillgänglig i testfallet.

Det finns även stöd för Java 5 i Springs JDBC modul. Genom att ärva klassen SimpleJdbcDaoSupport istället för JdbcDaoSupport i din DAO kan Java 5 metoder användas via klassen SimpleJdbcTemplate. Se exemplet nedan.

public List<Person> getPeople() {
    ParameterizedRowMapper<Person> rowMapper =
        new ParameterizedRowMapper<Person>() {
            public Person mapRow(ResultSet rs, int rowNum)
                    throws SQLException {

                return new Person(
                        rs.getString("first_name"),
                        rs.getString("last_name"),
                        rs.getString("phone_number"));
            }
    };

    return getSimpleJdbcTemplate().query(
            "select first_name, last_name, phone_number from person",
            rowMapper);
}

För mer information om Spring och JDBC rekommenderas Springdokumentationen och JavaDoc för JdbcTemplate.