Persistenza dei dati con Spring Data JPA

Nel post dedicato a JDBC e al JdbcTemplate abbiamo già esaminato il tema della connessione al database e della persistenza dei dati in un’applicazione basata su Spring.

In questa puntata facciamo un ulteriore passo avanti, esaminando uno scenario leggermente più complesso, ma sicuramente più comune in applicazioni “reali”.

Per questo tutorial riprendiamo la nostra cara SuperRubricaTelefonica! e implementiamo la registrazione degli utenti e il salvataggio dei contatti sfruttando Spring Data JPA.

ORM, JPA e il progetto Spring Data

Nell’articolo già citato avevo accennato al fatto che l’approccio basato su JDBC può tornare molto utile in applicazioni piuttosto semplici, basate su database con schemi altrettanto semplici. Nella maggior parte dei casi, però, ci troveremo a lavorare su strutture abbastanza articolate, con numerose tabelle relazionate logicamente tra loro.

In scenari complessi diventa molto utile l’impiego dell’Object-Relational Mapping (ORM), ovvero la possibilità, tramite strumenti software, di associare le tabelle presenti nel DB a corrispondenti oggetti di dominio. Il mapping, inoltre, ha il vantaggio di riportare le relazioni logiche esistenti tra le tabelle anche a livello applicativo.

La specifica JPA (Java Persistence API), definisce proprio un’interfaccia applicativa utile a costruire sistemi ORM basati su oggetti Java.

Esistono molte librerie e framework che implementano la specifica JPA e che ci permettono di effettuare facilmente il mapping delle nostre tabelle su entità; il più celebre è sicuramente il progetto Hibernate ORM.

Ma non finisce qui…

Uno dei principali progetti “spin off” derivati da Spring è Spring Data, framework nato con l’obiettivo di fornire un modello standard per l’implementazione del data access layer all’interno di un progetto Spring.

Spring Data definisce un set di API indipendente dalla sorgente dati utilizzata, fornendo l’implementazione di queste interfacce per molte delle più diffuse tecnologie di persistenza dei dati. Nella pagina principale del progetto è possibile trovare una lista di tutte le tecnologie supportate (relazionali e non) come JPA, MongoDB, Redis e molte altre.

Per il nostro progetto useremo Spring Data JPA, che implementa la specifica JPA tramite Hibernate e ci consente di lavorare con database relazioni.

Aggiungiamo le dipendenze

Per gestire il salvataggio e l’accesso ai dati utilizzando JPA dobbiamo aggiungere al pom.xml la seguente dipendenza:

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

Il pacchetto, oltre ad includere tutte le dipendenze necessarie per lavorare con Spring Data, predispone un set di autoconfigurazioni per i più diffusi motori di database relazionali.

Piccola nota Il pacchetto spring-boot-starter-data-jpa include già la dipendenza spring-boot-starter-jdbc, per cui se precedentemente stavate lavorando con JDBC e JdbcTemplate (come nel nostro caso) potete eliminare la vecchia dipendenza dal vostro pom.xml.

Anche in questo tutorial, continueremo ad utilizzare H2 come motore di database, per cui accertatevi che tra le vostre dipendenze ci sia:

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

Creiamo le Entity

Per “dialogare” con il DB avremo bisogno di definire delle entità che rappresentano gli oggetti di dominio della nostra applicazione.

Abbiamo già fatto qualcosa di simile lavorando con il JdbcTemplate; JPA “rafforza” e formalizza la dichiarazione delle entità all’interno della nostra applicazione tramite l’uso di Java annotation.

Se ad esempio riprendiamo la nostra classe Contact, già definita precedentemente, essa diventerà qualcosa di simile:

@Entity
@Table(name = "contacts")
public class Contact {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;
  private String firstName;
  private String lastName;
  private String phone;
  private String email;
  @ManyToOne
  private User user;

  // get + set

}

Esaminiamo alcune annotazioni (se conoscete JPA, le avrete già incontrate):

  • @Entity che identifica le classi-entità;
  • l’annotazione @Table che permette di specificare a quale tabella del DB fa riferimento l’entità;
  • l’annotazione @Id che specifica il campo associato alla chiave primaria della tabella;

La specifica JPA prevede inoltre una serie di annotazioni utili per definire le relazioni esistenti tra le varie classi/entità/tabelle. Questo, ad esempio, permette a strumenti come Hibernate di recuperare contestualmente tutti gli oggetti “collegati” tra loro, in base alle relazioni dichiarate.

Nel nostro caso, @ManyToOne indica la presenza di una relazione molti-a-uno tra l’entità Contact e l’entità User. Nel momento in cui andremo a recuperare un contatto dalla nostra rubrica, Hibernate inietterà nell’istanza di Contact anche l’oggetto User (l’utente che ha creato il contatto) associato/relazionato ad essa.

Fico, no?

Nota Se volete delegare al database engine la generazione degli ID per le vostre righe/entità ricordate di aggiungere l’annotation @GeneratedValue al campo contrassegnato come @Id, altrimenti in fase di salvataggio otterrete un’eccezione tipo questa:

org.hibernate.id.IdentifierGenerationException:
ids for this class must be manually assigned before calling save():
com.davioooh.srt.domain.Contact

Se non specificate l’auto-generazione degli ID, infatti, Hibernate si aspetterà che essi vengano assegnati esplicitamente dall’applicazione.

Definiamo i Repository

Dopo aver sistemato le nostre classi-entità, è il momento di sfruttare la magia di Spring Data per implementare i Repository.

Il data access layer della SuperRubricaTelefonica era stato inizialmente costruito sfruttando JdbcTemplate e un pizzico di SQL. Inoltre il mapping degli oggetti da e verso le tabelle del DB aveva richiesto un piccolo sforzo e l’utilizzo di appositi RowMapper. Niente di complicato, certo, ma Spring Data JPA ci assiste ulteriormente, facendo gran parte del lavoro per noi.

Concentriamoci nuovamente sui contatti e implementiamo il ContactRepository, questa volta sfruttando il nuovo fremework. Per fare ciò sarà sufficiente trasformare la classe ContactRepository in un’interfaccia e fare in modo che estenda il CrudRepository di Spring Data:

public interface ContactRepository extends CrudRepository<Contact, Long> {
}

Tutto qui… Fatto!

Non ci credete?

In effetti la cosa può lasciare abbastanza shoccati all’inizio. I meccanismi interni di Spring Data vengono delineati da Oliver Gierke (uno dei creatori del progetto) in questa risposta su Stack Overflow.

Cercherò di riassumere i concetti principali e di semplificare un po’ la faccenda…

Come funziona un Repository Spring Data

Il fulcro di Spring Data è costituito dalle interfacce Repository e CrudRepository. La prima funge da marker, identificando le interfacce delegate alla comunicazione con la sorgente dati; la seconda introduce le principali operazioni CRUD, tipiche di ogni Repository che si rispetti.

In Spring Data è possibile estendere il comportamento del CrudRepository in due modi: definendo Query Method o implementando esplicitamente metodi custom.

La “magia” principale introdotta da Spring Data è costituita proprio dai Query Method. Basterà infatti dichiarare dei semplici metodi nella nostra interfaccia-repository per costruire le query di cui abbiamo bisogno. Per far sì che il framework sappia come convertire i nostri metodi in query, la dichiarazione deve rispettare le regole sintattiche descritte nella documentazione.

Nel caso della nostra rubrica, ad esempio, dichiareremo in ContactRepository il seguente metodo:

List<Contact> findByUserId(long userId);

che Spring Data JPA provederà a convertire (utilizzando JPQL) in qualcosa del genere:

select c from Contact c where c.user.id = ?

Nel caso invece non riuscissimo a costruire la query utilizzando i Query Method, dovremo rimboccarci le maniche e implementare i metodi dichiarati nell’interfaccia, mediante il connettore o il manager specifico della nostra sorgente dati. Nel caso di JPA, utilizzeremo EntityManager.

Configurazioni aggiuntive

Come abbiamo già visto, lavorando con un embedded database come H2 non abbiamo bisogno di configurazioni particolari per ottenere un’applicazione subito funzionante: il pacchetto spring-boot-starter-data-jpa fa tutto il lavoro per noi.

Più spesso però ci capiterà di lavorare con motori di database un po’ più “consistenti”. Esempi comuni possono essere MySQL, PostgreSQL, Oracle DB o MS SqlServer. In questo caso avremo bisogno di un minimo di sforzo in più per indicare a Spring qual è la fonte dati da utilizzare.

Niente di preoccupante; ci basterà aggiungere nel nostro application.properties le seguenti proprietà:

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost/test
spring.datasource.username=dbuser
spring.datasource.password=dbpass

L’esempio riportato fa riferimento ad un database MySQL; per maggiori dettaglio vi rimando alla documentazione ufficiale.

Piccola nota Se volete visualizzare nei log il codice SQL generato da Hibernate, potete aggiungere anche queste proprietà:

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

In conclusione

Se siete arrivati al termine di questo interminabile post, vi ringrazio, sono commosso… :-)

Di cose da raccontare su Spring Data ce n’erano molte e ho cercato di sintetizzare il più possibile gli aspetti principali, utili per cominciare a lavorare con questo framework.

Ovviamente c’è molto più di quanto visto in questo tutorial e avremo modo d’incontrare nuovamente il progetto Spring Data (anche nelle sue vesti “non relazionali”) in futuri articoli.

Per il momento è tutto. Buon lavoro e alla prossima,

David