QUERY E SPRING DATA


Il vantaggio derivante dall'utilizzo di un Object Relational Mapping è quello di mappare ogni singolo oggetto di dominio con una tabella e di conseguenza, ragionando per entità, rendere indipendente il codice dallo specifico database che si sta utilizzando. 
Qualora in futuro volessimo cambiare la nostra base dati passando da PostgreSQL a SQL Server, basterà cambiare semplicemente il driver di connessione ed i relativi parametri di configurazione(url, username e password) all'interno dei file di properties senza modificare in alcun modo le queries costruite con l'ausilio dell'ORM.
La specifica JPA(Java Persistence API) definisce un'interfaccia utile allo sviluppo di ORM basati su oggetti Java: tra le numerose librerie e framework che consentono di effettuare questo mapping, il più celebre è sicuramente Hibernate ORM.
Nell'ambito dell'ecosistema Spring, nasce Spring Data JPA: esso implementa la specifica JPA tramite Hibernate con l'obiettivo di definire un set di Api indipendente dalla sorgente dati e fornendo per esso anche l'implementazione di interfacce relative alle più famose tecnologie per la persistenza dei dati.
Che significa tutto ciò? Vediamolo in un esempio. 
Per prima cosa aggiungiamo al pom.xml le seguenti dipendenze:
<dependency>
 <groupid>org.springframework.boot</groupid>
 <artifactid>spring-boot-starter-data-jpa</groupid>
</dependency>
<dependency>
 <groupid>com.h2database</groupid>
 <artifactid>h2</groupid>
</dependency>
Ora modelliamo un oggetto Book con gli attributi title, isbn, year of publication and publisher.
@Entity
@Table(name = "books")
public class Book {
  
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;
  private String title;
  private String isbn;
  private String publisher;
  private int yearOfPublication;

  // get + set

}
Fatto ciò, creiamo il nostro componente software di accesso ai dati. Come detto poc'anzi, Sping Data offre delle interfacce con un set di funzionalità già sviluppate, per cui il nostro compito sarà quello di dichiarare dei metodi all'interno dell'interfaccia senza dove ricorrere a query verbose:
@Repository
public interface BookRepository extends JpaRepository<Book> {

	List<Book> findByTitle(String title);

}
Per fare in modo che Spring Data sia in grado di interpretare i metodi da noi creati nel Repository, è necessario seguire le linee guida riportate nella documentazione ufficiale. Tuttavia l'idea di fondo è che, dati gli attributi di un oggetto di dominio, è possibile effettuare delle query di estrazione sulla base dei loro nomi. Nel nostro caso, l'oggetto Book ha un attributo title per cui è possibile "querare" tutti i libri che hanno quel nome con la naming Convention findByTitle.
Qualora volessimo recuperare tutti i libri pubblicati dall'editore "Manning" e con anno di pubblicazione il 1989, il metodo sarà il seguente:
List<Book> findByPublisherAndYearOfPublication(String publisher, int year);
Spring Data offre davvero tantissime opportunità ed è uno strumento potentissimo: non vi è cosa che non possa fare. Tuttavia, qualche problemino può nascere nel momento in cui la nostra base dati non è ben strutturata motivo per cui sono necessarie molte join o molti raggruppamenti.
Ecco perché, anche se l'utilizzo di query native è altamente sconsigliato, può emergere all'interno del progetto tale necessità. Un piccolo consiglio: fate attenzione alla formattazione della query stessa, nonché agli spazi tra una parola e l'altra qualora la stringa sia posizionata su più righe. I parametri posso essere passati in ingresso alla query sia per nome che per posizione:
@Query(value="select * from book a where a.title= :title", nativeQuery=true)
List<Book> getBooksByTitle(String title);
@Query(value="select * from book a where a.title= :?1", nativeQuery=true)
List<Book> getBooksByTitle(String title);
Se il risultato dell'interrogazione è un oggetto custom, ossia un oggetto per il quale non esiste una corrispondente tabella, è possibile utilizzare una Projection, ossia un interfaccia contenente solo i metodi get di accesso agli attributi ritornati dalla query. Essa quindi altro non è che un subset di una o più entità presenti a db e che nasce dall'esigenza di snellire il result set di una query:
public interface BookProjection { 
    String getTitle();
    String getIsbn();
}
Da cui ne deriva la seguente query:
@Query(value="select title,isbn from book a where a.publisher= :?1", nativeQuery=true)
List<Book> getBooksByPublisher(String publisher);
In questa panoramica di Spring Data, non poteva mancare un riferimento alle Specification. Lo scenario è il seguente: supponiamo che nella nostra applicazione è prevista la possibilità che l'utente utilizzatore possa effettuare una ricerca dei libri inserendo o meno dei filtri. Nel caso specifico:
  • title
  • isbn
  • publisher
  • year of publication
Se non viene selezionato nessun criterio di ricerca, la query recupera tutti i risultati; se, invece, la ricerca viene raffinata, i singoli criteri verranno aggiunti in AND conditon.
Per evitare di creare dei metodi con nomi super verbosi, è possibile creare una classe che si occupi di costruire il predicato da dare in pasto al repository.
Per prima cosa definiamo un DTO che wrappi i criteri di ricerca:
public class SearchBookDTO {

	private String title;
	private String publisher;
	private String isbn;
	private int year;

}
Fatto ciò creiamo la Specification nel seguente modo:
public class BookSpecification implements Specification<Book> {

	/**
	 *
	 */
	private static final long serialVersionUID = -1418612780146044594L;

	private final SearchBookDTO criteria;

	public BookSpecification(SearchBookDTO criteria) {
		this.criteria = criteria;
	}

	@Override
	public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
		
        List<Predicate> predicates = new ArrayList<>();
		if (criteria != null && StringUtils.hasText(criteria.getTitle())) {
			predicates.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("title")), "%" + criteria.getTitle().toLowerCase() + "%"));
		}
       	if (criteria != null && StringUtils.hasText(criteria.getPublisher())) {
			predicates.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("publisher")), "%" + criteria.getPublisher().toLowerCase() + "%"));
		}
        if (criteria != null && StringUtils.hasText(criteria.getIsbn())) {
			predicates.add(criteriaBuilder.eq(root.get("isbn"), criteria.getIsbn());
		}
        if (criteria != null && criteria.getYear() != null) {
			predicates.add(criteriaBuilder.eq(root.get("yearOfPublication"), criteria.getYear());
		}
		return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
	}

}
Il gioco è fatto. Nel service layer, basterà istanziare l'oggetto appena creato passando al costruttore i filtri inviati dall'utente utilizzatore e incapsulati nella classe SearchBookDTO:
var spec = new BookSpecification(filters);
Page<Book> books = bookRepository.findAll(spec, PageRequest.of(1, 10));
Tuttavia, affinchè tutto il "giro" funzioni correttamente è necessario che il repository estenda anche  JpaSpecificationExecutor come riportato di seguito:
@Repository
public interface BookRepository extends JpaRepository<Book>, JpaSpecificationExecutor<Book> {

	List<Book> findByTitle(String title);

}

Comments

Popular posts from this blog

AGGIORNAMENTO INVESTIMENTI 2° TRIMESTRE 2024

AGGIORNAMENTO INVESTIMENTI 1° TRIMESTRE 2024

HO COMPRATO UNO XIOAMI 14 ULTRA