Questo articolo è parte di una serie di articoli dedicati alla programmazione di Web App. Si tratta di un articolo divulgativo ed è pensato per essere di supporto e ripasso alle lezioni che tengo nel mondo reale. Buona lettura.
Immaginiamo di voler costruire un Ricettario. Cioè un’applicazione dove sia possibile ricercare, aggiungere, modificare e cancellare ricette all’interno di un database. Un po’ come se fosse il libro delle ricette di nonna, ma più smart.
Abbiamo creato la nostra form, i nostri controller e i nostri modelli, come spiegato negli articoli precedenti, ora non ci resta che mettere in piedi un database e scrivere gli algoritmi necessari a salvare, ricercare, cancellare e modificare i record di quest’ultimo. Sembra complicato? non spaventatevi: con Spring Boot è estremamente semplice!
1. Le dipendenze
Per prima cosa dobbiamo aggiornare il nostro pom.xml ed inserire le necessarie dipendenze per la gestione del database. Tutte le dipendenze di cui abbiamo bisogno possono essere recuperate online nel repository Maven.
Ci serviranno in tutto due dipendenze: una per la gestione delle JPA ed una specifica per il database che andremo ad utilizzare. La libreria per la gestione delle JPA è org.springframework.boot.spring-boot-starter-data-jpa, che include Hibernate con le sue dipendenze (eh si, una dipendenza può avere dipendenze a sua volta). In questo esempio illustrerò come usare database H2 o alternativamente un database MySQL.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
dipendenze per usare un database H2
La dipendenza com.h2database.h2 viene importata con scope runtime. Questo significa che avremo a disposizione il database solo durante l’esecuzione dell’applicazione.
Se al posto di un database H2 volete usare un database MySQL, allora le dipendenze da aggiungere in questo caso sono:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
dipendenze per usare un database MySQL
In entrambi i casi, viene installata la libreria org.springframework.boot.spring-boot-starter-data-jpa, che fornisce un substrato comune al nostro progetto permettendoci di astrarci dal database e trattare tutti (o quasi!) i database relazionali alla stessa maniera. Questo è appunto il “potere” delle JPA!
2. La configurazione
Configurare le risorse di un’applicazione Spring Boot è semplice: basta aggiornare il file application.properties, situato nella cartella src/main/resources. Questo file valorizza delle variabili “globali” interne all’applicazione che possono essere lette dalle classi Java. Quindi anche dalle librerie, che le usano per valorizzare le opportune variabili.
Aggiungiamo quindi le seguenti righe al file application.properties.
spring.datasource.url=jdbc:h2:file:~/test
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
application.properties per database H2
Le ultime due righe del file esposto qui sopra abilitano la console di configurazione e consultazione di H2, che sarà disponibile al path: http://localhost:8080/h2-console. La console per H2 è un po’ l’equivalente di quello che phpMyAdmin è per MySQL.
Se invece state usando un database MySQL, il file application.properties diventa:
spring.datasource.url=jdbc:mysql://localhost:3306/receips
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=create
application.properties per database MySQL
In questo caso, prestate attenzione al parametro spring.jpa.hibernate.ddl-auto, che può potenzialmente distruggere l’intero database. Quando è impostato su create il database viene distrutto e ricreato ogni volta che l’applicazione viene spenta e riaccesa. E’ un’impostazione molto comoda quando si tratta di sviluppare, ma estremamente dannosa in ambiente di produzione! Gli altri possibili valori di questo parametro sono: validate, update, create-drop e none.
3. Hibernate
Hibernate è un’implementazione delle JPA che si è affermata nel tempo come una delle migliori nel suo campo. E’ estremamente semplice ed intuitiva e si basa, come tutto Spring Boot, sulle @Annotation Java. In due parole si tratta di un ORM (Object-Relational Mapping) che ci permette di astrarre completamente il database, per vederlo soltanto come un sub-strato della nostra applicazione. Un substrato che “persiste” nel tempo tutte le istanze dei nostri modelli, a cui rivolgerci ogni volta che abbiamo bisogno di lavorare con i dati.
Riprendiamo quindi i modelli che abbiamo costruito negli articoli precedenti e trasformiamo questi modelli in Entity. Ovvero in qualcosa che le JPA riconoscono e che sono in grado di mappare su un database relazionale.
package com.example.demo.model;
public class Ingredient {
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Ingredient.java
package com.example.demo.model;
import java.util.List;
public class Recipe {
private String name;
private String description;
private List<Ingredient> ingredients;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public List<Ingredient> getIngredients() {
return ingredients;
}
public void setIngredients(List<Ingredient> ingredients) {
this.ingredients = ingredients;
}
}
Recipe.java
e trasformiamoli come segue:
package com.example.demo.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Ingredient {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
@Column
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.example.demo.model;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
@Entity
public class Recipe {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
@Column
private String name;
@Column
private String description;
@OneToMany
private List<Ingredient> ingredients;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public List<Ingredient> getIngredients() {
return ingredients;
}
public void setIngredients(List<Ingredient> ingredients) {
this.ingredients = ingredients;
}
}
L’etichetta @Entity comunica ad Hibernate che la classe è un “oggetto” del database. Quindi hibernate creerà una tabella con il nome della classe e una colonna per ciascun attributo della stessa. Il tipo di ogni colonna è derivato dal tipo dell’attributo corrispondente. L’attributo etichettato come @Id sarà la chiave primaria della tabella e generalmente si preferisce che sia un tipo numerico abbastanza grande (int o long). L’annotation @GeneratedValue(strategy = GenerationType.AUTO) fa sì che il campo sia incrementato e valorizzato autonomamente dal database.
Un database relazionale è noto appunto per le sue relazioni. Ovvero per il fatto che è capace di mantenere dei legami fra le sue tabelle in modo da garantire l’integrità del database. Per esempio, una ricetta è composta da uno o più ingredienti mentre ricette e ingredienti vengono salvati ciascuno su una specifica tabella. Un ingrediente può appartenere a più di una ricetta, ma ogni ricetta avrà un determinato ingrediente una ed una sola volta. Se si eliminano un po’ di ricette è possibile che qualche ingrediente presente nella tabella Ingredienti non sia più utilizzato da alcuna ricetta. Si potrebbe decidere di cancellare automaticamente gli Ingredienti non utilizzati da alcuna ricetta, o di lasciarli per il futuro… in ogni caso sono relazioni, appunto, che il database sa mantenere. Questo è il senso dell’ultima annotation: @OneToMany. Questa annotation comunica a Hibernate che fra le entity Ingredient e Recipe esiste una precisa relazione per cui ad una ricetta corrisponde uno o più ingredienti. Non ho espresso una relazione contraria, quindi avendo un ingrediente non posso da esso risalire alle ricette di cui fa parte (o meglio: posso, ma bisogna scrivere una query apposta).
I Repository
A questo punto manca solo un pezzo: una classe che faccia da raccordo di tutto quello che ho esposto sopra. In pratica una classe che potremo usare all’interno del nostro progetto per cercare, creare, modificare, eliminare entity dal database. In pratica un repository, ed è così che lo chiamaremo.
Creiamo quindi un apposito package e dichiariamo il repository. Avremo un repository per ogni Entity dichiarata:
package com.example.demo.repository;
import com.example.demo.model.Recipe;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RecipeRepositoryInterface extends JpaRepository<Recipe, Integer> {}
I lettori più attenti a questo punto potrebbero essere balzati sulla sedia, ed è lecito. Ho parlato di una classe che svolga le funzioni di repository e che faccia da interfaccia fra l’applicazione e il database, ma pur sempre una classe!
Come può un’interfaccia attendere al compito, dato che non è istanziabile? E’ presto detto: perché la nostra interfaccia ha il solo scopo di dare un tipo ai generici T, ID espressi da JpaRepository<T, ID>. Dopodiché, quando avremo bisogno del repository, Spring ci offrirà un’implementazione generica della nostra interfaccia, che è già presente nelle librerie data-jpa importate all’inizio: si tratta della classe SimpleJpaRepository, che implementa JpaRepository e contiene già tutti i metodi “classici” di interazione con il database. Ovvero ricerca, creazione, modifica e cancellazione di una o più entity, le cosiddette CRUD Operations.
Personalizzare il Repository
Abbiamo visto come la classe SimpleJpaRepository offre una comoda implementazione standard di un repository JPA. Ma come fare se invece abbiamo bisogno di personalizzare le nostre query o vogliamo aggiungere delle query specifiche per il nostro progetto?
Immaginiamo di aggiungere il metodo findIfNameContains alla nostra RecipeRepositoryInterface:
package com.example.demo.repository;
import com.example.demo.model.Recipe;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RecipeRepositoryInterface extends JpaRepository<Recipe, Integer> {
List<Recipe> findIfNameContains(String str);
@Query("SELECT c FROM Client c WHERE c.name LIKE :name")
Optional<Client> findByName(@Param("name") String name);
}
A questo punto la classe SimpleJpaRepository non è più essere un candidato ad essere un’istanza di RecipeRepositoryInterface, perché non ha il metodo custom che abbiamo aggiunto noi.
Quello che possiamo fare, però, è aggiungere una classe che implementi l’interfaccia RecipeRepositoryInterface, magari partendo proprio da SimpleJpaRepository
package com.example.demo.repository;
import java.util.List;
import javax.persistence.EntityManager;
import com.example.demo.model.Recipe;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public class RecipeRepository extends SimpleJpaRepository<Recipe, Integer> implements RecipeRepositoryInterface {
public RecipeRepository(Class<Recipe> domainClass, EntityManager em) {
super(domainClass, em);
}
@Autowired
private EntityManager entityManager;
public List<Recipe> findIfNameContains(String str) {
List<Recipe> list = entityManager
.createQuery("SELECT r FROM Recipe r WHERE r.name LIKE :str")
.setParameter("str", str)
.getResultList();
return list;
}
}
La query espressa assomiglia molto ad SQL, ma non è SQL. Si tratta invece di JPQL (Java Persistence Query Language). Un linguaggio del tutto simile ma che si basa sugli oggetti e sui loro attributi invece che su tabelle e colonne.
Con questo è tutto. Per ogni dubbio o domanda ricordate di commentare qui sotto o di mandarmi una mail. Alla prossima!