Introduction à Jinq avec Spring

1. Introduction

Jinq fournit une approche intuitive et pratique pour interroger des bases de données en Java. Dans ce didacticiel, nous allons explorer comment configurer un projet Spring pour utiliser Jinq et certaines de ses fonctionnalités illustrées avec des exemples simples.

2. Dépendances de Maven

Nous devrons ajouter la dépendance Jinq dans le fichier pom.xml :

 org.jinq jinq-jpa 1.8.22 

Pour Spring, nous ajouterons la dépendance Spring ORM dans le fichier pom.xml :

 org.springframework spring-orm 5.2.5.RELEASE 

Enfin, pour les tests, nous utiliserons une base de données en mémoire H2, donc ajoutons également cette dépendance, ainsi que spring-boot-starter-data-jpa au fichier pom.xml:

 com.h2database h2 1.4.200   org.springframework.boot spring-boot-starter-data-jpa 2.2.6.RELEASE 

3. Comprendre Jinq

Jinq nous aide à écrire des requêtes de base de données plus faciles et plus lisibles en exposant une API fluide basée en interne sur l'API Java Stream.

Voyons un exemple où nous filtrons les voitures par modèle:

jinqDataProvider.streamAll(entityManager, Car.class) .where(c -> c.getModel().equals(model)) .toList();

Jinq traduit l'extrait de code ci-dessus en une requête SQL de manière efficace , la requête finale de cet exemple serait donc:

select c.* from car c where c.model=?

Étant donné que nous n'utilisons pas de texte brut pour écrire des requêtes et que nous utilisons plutôt une API de type sécurisé, cette approche est moins sujette aux erreurs.

De plus, Jinq vise à permettre un développement plus rapide en utilisant des expressions communes et faciles à lire.

Néanmoins, il a quelques limitations dans le nombre de types et d'opérations que nous pouvons utiliser, comme nous le verrons ensuite.

3.1. Limites

Jinq ne prend en charge que les types de base dans JPA et une liste concrète de fonctions SQL. Il fonctionne en traduisant les opérations lambda en une requête SQL native en mappant tous les objets et méthodes dans un type de données JPA et une fonction SQL.

Par conséquent, nous ne pouvons pas nous attendre à ce que l'outil traduise chaque type personnalisé ou toutes les méthodes d'un type.

3.2. Types de données pris en charge

Voyons les types de données et les méthodes pris en charge:

  • String - méthodes equals () , compareTo () uniquement
  • Types de données primitifs - opérations arithmétiques
  • Enums et classes personnalisées - prend en charge les opérations == et! = Uniquement
  • java.util.Collection - contient ()
  • Date API - méthodes equals () , before () , after () uniquement

Remarque: si nous voulions personnaliser la conversion d'un objet Java en un objet de base de données, nous aurions besoin d'enregistrer notre implémentation concrète d'un AttributeConverter dans Jinq.

4. Intégration de Jinq avec Spring

Jinq a besoin d' un EntityManager exemple pour obtenir le contexte de persistance. Dans ce didacticiel, nous présenterons une approche simple avec Spring pour faire fonctionner Jinq avec EntityManager fourni par Hibernate.

4.1. Interface du référentiel

Spring utilise le concept de référentiels pour gérer les entités. Regardons notre interface CarRepository où nous avons une méthode pour récupérer une voiture pour un modèle donné:

public interface CarRepository { Optional findByModel(String model); }

4.2. Référentiel de base abstraite

Ensuite, nous aurons besoin d'un référentiel de base pour fournir toutes les fonctionnalités Jinq:

public abstract class BaseJinqRepositoryImpl { @Autowired private JinqJPAStreamProvider jinqDataProvider; @PersistenceContext private EntityManager entityManager; protected abstract Class entityType(); public JPAJinqStream stream() { return streamOf(entityType()); } protected  JPAJinqStream streamOf(Class clazz) { return jinqDataProvider.streamAll(entityManager, clazz); } }

4.3. Implémentation du référentiel

Maintenant, tout ce dont nous avons besoin est une Jinq EntityManager instance et la classe de type d'entité.

Voyons l' implémentation du référentiel Car en utilisant notre référentiel de base Jinq que nous venons de définir:

@Repository public class CarRepositoryImpl extends BaseJinqRepositoryImpl implements CarRepository { @Override public Optional findByModel(String model) { return stream() .where(c -> c.getModel().equals(model)) .findFirst(); } @Override protected Class entityType() { return Car.class; } }

4.4. Câblage du JinqJPAStreamProvider

Afin de câbler l' instance JinqJPAStreamProvider , nous ajouterons la configuration du fournisseur Jinq:

@Configuration public class JinqProviderConfiguration { @Bean @Autowired JinqJPAStreamProvider jinqProvider(EntityManagerFactory emf) { return new JinqJPAStreamProvider(emf); } }

4.5. Configuration de l'application Spring

La dernière étape consiste à configurer notre application Spring en utilisant Hibernate et notre configuration Jinq. Pour référence, consultez notre fichier application.properties , dans lequel nous utilisons une instance H2 en mémoire comme base de données:

spring.datasource.url=jdbc:h2:~/jinq spring.datasource.username=sa spring.datasource.password= spring.jpa.hibernate.ddl-auto=create-drop

5. Guide de requête

Jinq fournit de nombreuses options intuitives pour personnaliser la requête SQL finale avec select, where, join et plus. Notez que ceux-ci ont les mêmes limitations que nous avons déjà introduites ci-dessus.

5.1. Où

La clause where permet d'appliquer plusieurs filtres à une collection de données.

Dans l'exemple suivant, nous voulons filtrer les voitures par modèle et par description:

stream() .where(c -> c.getModel().equals(model) && c.getDescription().contains(desc)) .toList();

Et c'est le SQL que Jinq traduit:

select c.model, c.description from car c where c.model=? and locate(?, c.description)>0

5.2. Sélectionner

In case we want to retrieve only a few columns/fields from the database, we need to use the select clause.

In order to map multiple values, Jinq provides a number of Tuple classes with up to eight values:

stream() .select(c -> new Tuple3(c.getModel(), c.getYear(), c.getEngine())) .toList()

And the translated SQL:

select c.model, c.year, c.engine from car c

5.3. Joins

Jinq is able to resolve one-to-one and many-to-one relationships if the entities are properly linked.

For example, if we add the manufacturer entity in Car:

@Entity(name = "CAR") public class Car { //... @OneToOne @JoinColumn(name = "name") public Manufacturer getManufacturer() { return manufacturer; } }

And the Manufacturer entity with the list of Cars:

@Entity(name = "MANUFACTURER") public class Manufacturer { // ... @OneToMany(mappedBy = "model") public List getCars() { return cars; } }

We're now able to get the Manufacturer for a given model:

Optional manufacturer = stream() .where(c -> c.getModel().equals(model)) .select(c -> c.getManufacturer()) .findFirst();

As expected, Jinq will use an inner join SQL clause in this scenario:

select m.name, m.city from car c inner join manufacturer m on c.name=m.name where c.model=?

In case we need to have more control over the join clauses in order to implement more complex relationships over the entities, like a many-to-many relation, we can use the join method:

List
    
      list = streamOf(Manufacturer.class) .join(m -> JinqStream.from(m.getCars())) .toList()
    

Finally, we could use a left outer join SQL clause by using the leftOuterJoin method instead of the join method.

5.4. Aggregations

All the examples we have introduced so far are using either the toList or the findFirst methods – to return the final result of our query in Jinq.

Besides these methods, we also have access to other methods to aggregate results.

For example, let's use the count method to get the total count of the cars for a concrete model in our database:

long total = stream() .where(c -> c.getModel().equals(model)) .count()

And the final SQL is using the count SQL method as expected:

select count(c.model) from car c where c.model=?

Jinq also provides aggregation methods like sum, average, min, max, and the possibility to combine different aggregations.

5.5. Pagination

In case we want to read data in batches, we can use the limit and skip methods.

Let's see an example where we want to skip the first 10 cars and get only 20 items:

stream() .skip(10) .limit(20) .toList()

And the generated SQL is:

select c.* from car c limit ? offset ?

6. Conclusion

On y va. Dans cet article, nous avons vu une approche pour configurer une application Spring avec Jinq en utilisant Hibernate (au minimum).

Nous avons également brièvement exploré les avantages de Jinq et certaines de ses principales caractéristiques.

Comme toujours, les sources peuvent être trouvées sur GitHub.