Utilizzare Spring nelle web application – parte 10

Vediamo in questo appuntamento come modificare la nostra applicazione per fare in modo che utilizzi, per il salvataggio e la lettura dei dati, il database creato nell’articolo precedente.

Configurazione

Per prima cosa dobbiamo creare il file src/main/resources/jdbc.properties con questo contenuto:

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost/arteraspringtutorial
jdbc.username=[utente]
jdbc.password=[password]

sostituendo [utente] e [password] con i valori appropriati per il proprio database.

Questo file memorizza i dati di accesso al database, e verrà utilizzato anche dall’applicazione finale; ricordiamo infatti che i file presenti nella directory src/main/resources/ vengono copiati nel classpath, ovvero nella directory /WEB-INF/classes della web application.

Fatto ciò, passiamo a src/main/resources/applicationContext.xml, aggiungendo queste righe:

classpath:jdbc.properties

Alcune doverose considerazioni:

  • il bean con id=”propertyConfigurer” indica a Spring di caricare le properties contenute in jdbc.properties;
  • il bean con id=”dataSource” è un’implementazione di javax.sql.DataSource. L’id “dataSource” è una convenzione di Spring per designare il dataSource dell’applicazione, nel caso ve ne fosse solo uno;
  • il bean con id=”transactionManager” definisce il componente che si occuperà di coordinare le transazioni delle classi annotate come @Service o @Transactional.

Spring JDBC

Questa libreria è già inclusa nelle dipendenze del progetto (spring-jdbc) e contiene diverse classi di utilità che consentono di gestire in maniera molto elegante e affidabile le complesse (e decisamente poco eleganti e usabili) JDBC API, consentendo al programmatore di concentrarsi sulla realizzazione delle query e non su aspetti “burocratici”: apertura/chiusura connessioni e delle transazioni, corretta gestione delle eccezioni, iterazione sui risultati, e molto altro.

org.springframework.jdbc.core.JdbcTemplate è la classe più importante di questo package e come molte delle classi di utilità di Spring utilizza il Template Method design pattern. La documentazione ufficiale di Spring la descrive molto bene (nda: traduzione):

Semplifica l’utilizzo di JDBC e aiuta a evitare gli errori più comuni. Si occupa di gestire correttamente il workflow delle JDBC API, lasciando al codice applicativo il compito di fornire l’SQL e di estrarre i risultati. Questa classe esegue query o update SQL, inizializzando l’iterazione sui ResultSet, catturando le eccezioni JDBC e traducendole nelle corrispettive generiche e più informative definite nel package org.springframework.dao.

Chi ha anche solo un minimo di esperienza con le macchinose JDBC API ha certamente idea di cosa vuol dire gestirne correttamente il workflow e siamo sicuri che apprezzerà le funzionalità fornite da JdbcTemplate!

InsertionDao

Per chi voglia implementare dei DAO utilizzando le JDBC API, Spring mette a disposizione una classe astratta: org.springframework.jdbc.core.support.JdbcDaoSupport. Estendendola, e configurando un dataSource, avremo a disposizione un DAO con un’istanza già pronta e funzionante di JdbcTemplate: nessun bisogno di configurare alcunché, nessun bisogno di specificare transazioni (gestite da Spring), nessuna gestione di eccezioni JDBC. Molto comodo!

Ecco quindi come cambia la nostra implementazione di it.artera.springtut.dao.InsertionDao:

package it.artera.springtut.dao;

import it.artera.springtut.model.Insertion;

import java.util.List;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.stereotype.Repository;

@Repository
public class InsertionDao extends JdbcDaoSupport {

    private RowMapper<?> insertionRowMapper;

    public InsertionDao() {
        this.insertionRowMapper = new InsertionRowMapper();
    }

    @Autowired
    public void init(DataSource dataSource) {
        setDataSource(dataSource);
    }

    /**
     * Carica gli ultimi 5 annunci inseriti nel database
     * @return
     */
    @SuppressWarnings("unchecked")
    public List<Insertion> getMostRecentInsertions() {
        StringBuilder sql = new StringBuilder(255);
        sql.append("select");
        addSelectFields(sql);
        sql.append(" from insertions as i");
        sql.append(" order by i.creationDate desc");
        sql.append(" limit 5");

        return (List<Insertion>) getJdbcTemplate().query(sql.toString(), insertionRowMapper);
    }

    /**
     * Carica l'inserzione con l'ID specificato
     * @param id
     * @return
     */
    public Insertion getInsertion(long id) {
        StringBuilder sql = new StringBuilder(255);
        sql.append("select");
        addSelectFields(sql);
        sql.append(" from insertions as i");
        sql.append(" where id = " + id);

        return (Insertion) getJdbcTemplate().queryForObject(sql.toString(), insertionRowMapper);
    }

    private void addSelectFields(StringBuilder sql) {
        sql.append(" i.id, i.title, i.description, i.photo, i.price, i.creationDate");
    }
}

Da notare:

  • le string SQL vengono create tramite degli StringBuilder per la massima flessibilità;
  • ai metodi query() e queryForObject() di JdbcTemplate viene passata un’istanza di RowMapper (vedi oltre per il sorgente). Questa verrà utilizzata per mappare correttamente i dati contenuti nel ResultSet nelle property delle istanze di oggetti Insertion;

Ecco quindi il sorgente di it.artera.springtut.dao.InsertionRowMapper, l’implementazione di RowMapper specifica per la nostra classe:

package it.artera.springtut.dao;

import it.artera.springtut.model.Insertion;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;

/**
 * Utilizzato per mappare le SELECT sql su oggetti Java, l'ordine dei campi
 *  lo stesso presente nella classe java {@link Insertion}
 */
class InsertionRowMapper implements RowMapper<Insertion> {
    public Insertion mapRow(ResultSet rs, int rowNum) throws SQLException {
        Insertion insertion = new Insertion();
        int i = 1;
        insertion.setId(rs.getLong(i++));
        insertion.setTitle(rs.getString(i++));
        insertion.setDescription(rs.getString(i++));
        insertion.setPhoto(rs.getString(i++));
        insertion.setPrice(rs.getBigDecimal(i++));
        insertion.setCreationDate(rs.getDate(i++));

        return insertion;
    }
}

 

Verifica

È arrivato il momento di fare una verifica, possiamo lanciare tutti i test dell’applicazione con il comando:

mvn test

oppure possiamo semplicemente verificare il solo InsertionDao eseguendo la classe di test InsertionDaoTest:

mvn test -Dtest=InsertionDaoTest

Se avete seguito correttamente le istruzioni, dovreste avere un bel messaggio BUILD SUCCESSFUL. Abbiamo infatti modificato solamente l’implementazione del nostro DAO, ma non l’interfaccia; quindi, a rigor di logica, i test (e in generale chi usa il DAO) dovrebbero continuare a funzionare normalmente, senza richiedere alcuna modifica.

Linkografia