Accesso ai dati con JdbcTemplate di Spring

Torniamo a lavorare sulla nostra SuperRubricaTelefonica!

In questo post vediamo come implementare il salvataggio dei dati, sfruttando l’interfaccia JDBC e l’utile classe di supporto JdbcTemplate di Spring.

Per questo tutorial utilizzeremo inoltre il database H2, ottimo per cominciare a fare piccoli esperimenti con il data access layer della nostra applicazione.

Aggiungiamo le dipendenze

Per implementare il salvataggio dei contatti nella nostra rubrica abbiamo bisogno di aggiungere un paio di dipendenze al pom.xml.

Innanzitutto, ci serve il pacchetto con le auto-configurazioni di Spring Boot relative a JDBC:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

Includendo il modulo spring-boot-starter-jdbc, Spring Boot ci fornisce una serie di facilitazioni per quel che riguarda la connessione al database e l’inizializzazione dello stesso, in particolare:

  • fornisce supporto per diversi database embedded (tra cui H2), auto-configurando la connessione;
  • registra nell’application context un oggetto di tipo JdbcTemplate, utile per eseguire operazioni sul DB;
  • configura un Transaction Manager per il supporto e l’implementazione delle transazioni durante l’accesso ai dati;
  • inizializza lo schema del database utilizzando il file schema.sql (se presente).

A questo punto è il momento di configurare la nostra sorgente di dati.

Il database

Come già accennato, per questo progetto utilizzeremo H2, un simpatico database engine scritto in Java, che può essere integrato direttamente all’interno di un’applicazione importandolo come dipendenza:

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
</dependency>

Nota Benché H2 possa essere utilizzato come un motore di database standalone (previa installazione), comunemente viene utilizzato in modalità in-memory, integrandolo all’interno di applicazioni Java. I dati salvati saranno mantenuti in memoria fintanto che l’applicazione non verrà riavviata: questo ci torna utile in fase di sviluppo e di debug.

Per inizializzare il nostro DB durante l’avvio dell’applicazione dobbiamo creare un file schema.sql nella cartella src/main/resources del progetto:

CREATE TABLE contacts
(
  id bigint NOT NULL AUTO_INCREMENT,
  first_name varchar(100) NOT NULL,
  last_name varchar(100) NOT NULL,
  phone varchar(100) NOT NULL,
  email varchar(100) DEFAULT NULL,
  PRIMARY KEY (id)
);

Come prevedibile, lo schema del nostro database sarà inizialmente molto semplice e comprenderà una singola tabella, quella dei contatti.

Repository e accesso ai dati

L’accesso ai dati in applicazione web è comunemente implementato tramite oggetti DAO o Repository.

Anche Spring riprende questo design pattern e definisce una specifica tipologia di componenti, identificati dall’annotazione @Repository.

Un Repository, è solitamente associato ad un singolo oggetto di dominio ed è responsabile di tutte le operazioni di recupero e salvataggio legate a questa entità.

Nel nostro caso, il dominio dell’applicazione si limita ad un solo oggetto, il contatto, che andremo a rappresentare con la classe seguente:

public class Contact {
  private long id;
  private String firstName;
  private String lastName;
  private String phone;
  private String email;

  // get + set

}

Le API JDBC sono storicamente note per la loro verbosità, che spesso si traduce in errori e bug difficili da individuare. La classe JdbcTemplate di Spring semplifica notevolmente l’utilizzo di queste interfacce, mettendo a disposizione metodi per l’implementazione delle principali operazioni legate al DB.

Di seguito riporto un estratto della classe Repository associata a Contact:

@Repository
public class ContactRepository {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  public Contact findById(long id) {
  return jdbcTemplate.queryForObject(
    "select * from contacts where id = ?",
    new ContactRowMapper(),
    id);
  }

  // ...

  static class ContactRowMapper implements RowMapper<Contact> {
    @Override
    public Contact mapRow(ResultSet resultSet, int i) throws SQLException {
      Contact contact = new Contact();
      contact.setId(resultSet.getLong("id"));
      contact.setFirstName(resultSet.getString("first_name"));
      contact.setLastName(resultSet.getString("last_name"));
      contact.setPhone(resultSet.getString("phone"));
      contact.setEmail(resultSet.getString("email"));
      return contact;
    }
  }
}

Esaminiamo meglio il codice.

Il metodo findById ci consente di recuperare un contatto precedentemente salvato. Come vedete sfruttiamo il metodo queryForObject del JdbcTemplate per eseguire una query che restituirà un’unica riga del DB.

La query è parametrizzata e possiamo passare i valori della where condition in modo dinamico.

Altro aspetto interessante è l’utilizzo del RowMapper che consente di convertire le righe restituite dalla query in oggetti del nostro dominio applicativo.

Il salvataggio di un nuovo contatto è invece gestito dal metodo create:

public Contact create(Contact contact)
{
  String sql = "insert into contacts (first_name, last_name, phone, email) values (?, ?, ?, ?)";

  KeyHolder keyHolder = new GeneratedKeyHolder();
  jdbcTemplate.update(new PreparedStatementCreator() {
    @Override
    public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
      PreparedStatement preparedStatement
          = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
      preparedStatement.setString(1, contact.getFirstName());
      preparedStatement.setString(2, contact.getLastName());
      preparedStatement.setString(3, contact.getPhone());
      preparedStatement.setString(4, contact.getEmail());
      return preparedStatement;
    }
  }, keyHolder);

  contact.setId(keyHolder.getKey().longValue());
  return contact;
}

In questo caso il JdbcTemplate fa uso di una PreparedStatement per generare l’istruzione SQL da eseguire.

Nota interessante è l’utilizzo dell’oggetto di tipo KeyHolder per recuperare l’ID generato per la nuova riga inserita.

Ma dove sono i dati?

Mentre noi salviamo e rileggiamo i nostri contatti telefonici, H2 sta facendo il lavoro sporco, dietro le quinte.

Ma dove sono effettivamente i dati e le tabelle? In fin dei conti non abbiamo installato nulla…

I dati e la struttura del DB risiedono nella memoria RAM del nostro computer e vengono ricreati ad ogni avvio dell’applicazione.

Se vi state chiedendo se è possibile esplorare le tabelle del database, la risposta è ed è anche molto semplice.

H2 dispone di una console di gestione dei DB che consente di effettuare query e alterare la struttura della base dati. Spring Boot configura automaticamente anche questo utile strumento; la url per accedere alla console è: /h2-console.

Di default, la stringa di connessione da utilizzare per connettersi al DB è la seguente:

jdbc:h2:mem:testdb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE

Se provate ad accedere alla console, troverete la tabella contacts con i contatti precedentemente inseriti.

H2 Console

In conclusione

Abbiamo visto come sfruttare la classe JdbcTemplate di Spring per implementare, in modo piuttosto semplice, le operazioni di scrittura e recupero dei dati nella nostra web application.

L’utilizzo di JDBC e di JdbcTemplate è sicuramente consigliato nel caso di database semplici, con poche tabelle e/o poche relazioni; lavorando con strutture più complesse, però, l’approccio descritto in questo post potrebbe non essere la scelta migliore.

In molti casi risulta utile trasporre le relazioni presenti nel DB anche negli oggetti di dominio, utilizzando un ORM.

È in scenari come questi che il progetto Spring Data può fare la differenza in termini di semplificazione e produttività. Ma di questo parleremo meglio un’altra volta… ;-)

A presto,

David