Même si le générateur de critères JPA est expressif, les requêtes JPA sont souvent tout aussi détaillées et l’API elle-même peut être peu intuitive à utiliser, en particulier pour les nouveaux arrivants. Dans l’écosystème Quarkus, Panache est un remède partiel à ces problèmes lors de l’utilisation d’Hibernate. Pourtant, je me retrouve à jongler avec les méthodes d’assistance de Panache, les énumérations préconfigurées et les chaînes brutes lors de la composition de tout sauf de la plus simple des requêtes. Vous pourriez prétendre que je suis simplement inexpérimenté et impatient ou, au lieu de cela, reconnaître que l’API parfaite est facile à utiliser pour tout le monde. Ainsi, l’expérience utilisateur d’écriture de requêtes JPA peut être encore améliorée dans cette direction.
Introduction
L’un des défauts restants est que les chaînes brutes ne sont pas de type sécurisé par nature, ce qui signifie que mon IDE me rejette l’aide de la complétion de code et me souhaite au mieux bonne chance. En revanche, Quarkus facilite les relances d’applications en une fraction de seconde pour émettre des verdicts rapides sur mon code. Et rien ne vaut la joie sincère et la véritable surprise lorsque j’ai composé une requête de travail sur la cinquième, plutôt que la dixième, tentative…
Dans cet esprit, nous avons construit la bibliothèque open-source JPAstreamer pour rendre le processus d’écriture des requêtes Hibernate plus intuitif et moins chronophage tout en laissant intacte votre base de code existante. Il atteint cet objectif en permettant aux requêtes d’être exprimées sous forme de flux Java standard. Lors de l’exécution, JPAstreamer traduit le pipeline de flux en une requête HQL pour une exécution efficace et évite de matérialiser autre chose que les résultats pertinents.
Prenons un exemple : dans une base de données aléatoire, il existe une table appelée Person
représenté dans une application Hibernate par la norme suivante Entity
:
@Entity
@Table(name = "person")
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "person_id", nullable = false, updatable = false)
private Integer actorId;
@Column(name = "first_name", nullable = false, columnDefinition = "varchar(45)")
private String firstName;
@Column(name = "last_name", nullable = false, columnDefinition = "varchar(45)")
private String lastName;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
// Getters for all fields will follow from here
}
Pour aller chercher le Person
avec un identifiant de 1 en utilisant JPAstreamer, tout ce dont vous avez besoin est le suivant :
@ApplicationScoped
public class PersonRepository {
@PersistenceContext
EntityManagerFactory entityManagerFactory;
private final JPAStreamer jpaStreamer;
public PersonRepository EntityManagerFactory entityManagerFactory) {
jpaStreamer = JPAStreamer.of(entityManagerFactory); <1>
}
@Override
public Optional<Person> getPersonById(int id) {
return this.jpaStreamer.from(Person.class) <2>
.filter(Person$.personId.equal(id)) <3>
.findAny();
}
}
<1> Initialisez JPAstreamer en une seule ligne, le fournisseur JPA sous-jacent gère la configuration de la base de données.
<2> La source du flux est définie pour être le Person
tableau.
<3> L’opération de filtrage est traitée comme un SQL WHERE
clause et la condition est exprimée de manière sécurisée avec les prédicats JPAstreamer (plus à ce sujet à suivre).
Bien qu’il semble que JPAstreamer fonctionne sur tous Person
objets, le pipeline est optimisé pour une seule requête, dans ce cas :
select
person0_.person_id as person_id1_0_,
person0_.first_name as first_na2_0_,
person0_.last_name as last_nam3_0_,
person0_.created_at as created_4_0_,
from
person person0_
where
person0_.person_id=1
Ainsi, seul le Person
correspondant aux critères de recherche est toujours matérialisé.
Ensuite, nous pouvons regarder un exemple plus complexe dans lequel je recherche Person
est composé d’un prénom se terminant par un « A » et d’un nom de famille commençant par un « B ». Les correspondances sont triées principalement par prénom et deuxièmement par nom de famille. Je décide en outre d’appliquer un décalage de 5, en excluant les cinq premiers résultats, et de limiter le total des résultats à 10. Voici le pipeline de flux pour accomplir cette tâche :
List<Person> list = jpaStreamer.stream(Person.class)
.filter(Person$.firstName.endsWith("A").and(Person$.lastName.startsWith("B"))) <1>
.sorted(Person$.firstName.comparator().thenComparing(Person$.lastName.comparator())) <2>
.skip(5) <3>
.limit(10) <4>
.collect(Collectors.toList())
<1> Les filtres peuvent être combinés avec les opérateurs et/ou.
<2> Filtrez facilement sur une ou plusieurs propriétés.
<3> Passer les 5 premières personnes.
<4> Retournez au plus 10 personnes.
Dans le contexte des requêtes, les opérateurs de flux filtrer, trier, limiter et ignorer ont tous un mappage naturel qui rend la requête résultante expressive et intuitive à lire tout en restant compacte.
Cette requête est traduite par JPAstreamer en la déclaration HQL suivante :
select
person0_.person_id as person_id1_0_,
person0_.first_name as first_na2_0_,
person0_.last_name as last_nam3_0_,
person0_.created_at as created_4_0_,
from
person person0_
where
person0_.person_id=1
where
(person0_.first_name like ?)
and (person0_.last_name like ?)
order by
person0_.first_name asc,
person0_.last_name asc limit ?, ?
Comment fonctionne JPAstreamer
Bon, ça a l’air simple. Mais comment ça marche? JPAstreamer utilise un processeur d’annotation pour former un méta-modèle au moment de la compilation. Il inspecte toutes les classes marquées avec l’annotation JPA standard @Entity
et pour chaque entité Foo.class
un correspondant Foo$.class
est créé. Les classes générées représentent les attributs d’entité comme Fields
utilisé pour former des prédicats sur le formulaire User$.firstName.startsWith("A")
qui peut être interprété par l’optimiseur de requête de JPAstreamer.
Il convient de répéter que JPAstreamer ne modifie ni ne perturbe la base de code existante, mais étend simplement l’API pour gérer les requêtes de flux Java.
Installation de l’extension JPAstreamer
JPAstreamer est installé comme n’importe quelle autre extension Quarkus, en utilisant une dépendance Maven :
<dependency>
<groupId>io.quarkiverse.jpastreamer</groupId>
<artifactId>quarkus-jpastreamer</artifactId>
<version>1.0.0</version>
</dependency>
Une fois la dépendance ajoutée, reconstruisez votre application Quarkus pour déclencher le processeur d’annotations de JPAstreamer. L’installation est terminée une fois que les champs générés résident dans /target/generated-sources
; vous les reconnaîtrez par le $ final dans les noms de classe, par exemple, Person$.class
.
Note: JPAstreamer nécessite un fournisseur JPA sous-jacent, tel que Hibernate. Pour cette raison, JPAstreamer n’a pas besoin de configuration supplémentaire car l’intégration de la base de données est prise en charge par le fournisseur JPA.
JPAstreamer et Panache
Tout fan de Panache remarquera que JPAstreamer partage certains de ses objectifs avec Panache, en simplifiant de nombreuses requêtes courantes. Pourtant, JPAstreamer se distingue en instillant plus de confiance dans les requêtes grâce à son interface de flux de type sécurisé. Cependant, personne n’est obligé de faire un choix car Panache et JPAstreamer fonctionnent de manière transparente l’un à côté de l’autre.
Note: Voici un exemple d’application Quarkus qui utilise à la fois JPAstreamer et Panache.
Au moment de la rédaction, JPAstreamer ne prend pas en charge le modèle d’enregistrement actif de Panache, car il s’appuie sur les entités JPA standard pour générer son méta-modèle. Cela changera probablement dans un proche avenir.
Résumé
JPA en général et Hibernate ont grandement simplifié l’accès à la base de données des applications, mais son API force parfois une complexité inutile. Avec JPAstreamer, vous pouvez utiliser JPA tout en gardant votre base de code propre et maintenable.