Introduction à Querydsl

Introduction à Querydsl

1. introduction

Ceci est un article d'introduction pour vous familiariser avec la puissante APIQuerydsl pour la persistance des données.

Le but ici est de vous donner les outils pratiques pour ajouter Querydsl à votre projet, comprendre la structure et le but des classes générées et comprendre comment écrire des requêtes de base de données sécurisées pour les scénarios les plus courants.

2. Le but de Querydsl

Les infrastructures de mappage relationnel-objet sont au cœur d'Enterprise Java. Celles-ci compensent l'inadéquation entre l'approche orientée objet et le modèle de base de données relationnelle. Ils permettent également aux développeurs d’écrire du code de persistance et une logique de domaine plus propres et plus concis.

Toutefois, l'un des choix de conception les plus difficiles pour une structure ORM est l'API permettant de générer des requêtes correctes et adaptées au type.

Hibernate (et le standard JPA étroitement lié), l’un des frameworks Java ORM les plus largement utilisés, propose un langage de requête basé sur des chaînes, HQL (JPQL), très similaire à SQL. Les inconvénients évidents de cette approche sont le manque de sécurité de type et l'absence de vérification de requête statique. De même, dans des cas plus complexes (par exemple, lorsque la requête doit être construite à l'exécution en fonction de certaines conditions), la construction d'une requête HQL implique généralement la concaténation de chaînes qui sont généralement très dangereuses et sujettes aux erreurs.

La norme JPA 2.0 a apporté une amélioration sous la forme deCriteria Query API - une nouvelle méthode sécurisée de création de requêtes qui tirait parti des classes de métamodèles générées lors du prétraitement des annotations. Malheureusement, en tant que pionnier dans son essence, l’API Criteria Query s’est révélée très prolixe et pratiquement illisible. Voici un exemple du didacticiel Java EE pour générer une requête aussi simple queSELECT p FROM Pet p:

EntityManager em = ...;
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery cq = cb.createQuery(Pet.class);
Root pet = cq.from(Pet.class);
cq.select(pet);
TypedQuery q = em.createQuery(cq);
List allPets = q.getResultList();

Pas étonnant qu'une bibliothèqueQuerydsl plus adéquate soit rapidement apparue, basée sur la même idée de classes de métadonnées générées, mais implémentée avec une API fluide et lisible.

3. Génération de classe Querydsl

Commençons par générer et explorer les métaclasses magiques qui représentent l’API fluide de Querydsl.

3.1. Ajout de Querydsl à Maven Build

Inclure Querydsl dans votre projet est aussi simple que d'ajouter plusieurs dépendances à votre fichier de construction et de configurer un plugin pour le traitement des annotations JPA. Commençons par les dépendances. La version des bibliothèques Querydsl doit être extraite dans une propriété distincte dans la section<project><properties>, comme suit (pour la dernière version des bibliothèques Querydsl, vérifiez le référentielMaven Central):


    4.1.3

Ensuite, ajoutez les dépendances suivantes à la section<project><dependencies> de votre fichierpom.xml:



    
        com.querydsl
        querydsl-apt
        ${querydsl.version}
        provided
    

    
        com.querydsl
        querydsl-jpa
        ${querydsl.version}
    

La dépendancequerydsl-apt est un outil de traitement des annotations (APT) - implémentation de l'API Java correspondante qui permet le traitement des annotations dans les fichiers source avant qu'elles ne passent à l'étape de compilation. Cet outil génère les types appelés Q-types - classes qui se rapportent directement aux classes d'entité de votre application, mais qui sont préfixées de la lettre Q. Par exemple, si vous avez une classeUser marquée avec l'annotation@Entity dans votre application, le type Q généré résidera dans un fichier sourceQUser.java.

La portéeprovided de la dépendancequerydsl-apt signifie que ce fichier jar doit être rendu disponible uniquement au moment de la construction, mais pas inclus dans l'artefact d'application.

La bibliothèque querydsl-jpa est le Querydsl lui-même, conçu pour être utilisé avec une application JPA.

Pour configurer le plugin de traitement des annotations qui tire parti dequerydsl-apt, ajoutez la configuration de plugin suivante à votre pom - dans l'élément<project><build><plugins>:


    com.mysema.maven
    apt-maven-plugin
    1.1.3
    
        
            
                process
            
            
                target/generated-sources/java
                com.querydsl.apt.jpa.JPAAnnotationProcessor
            
        
    

Ce plugin s'assure que les types Q sont générés pendant l'objectif de processus de la génération Maven. La propriété de configurationoutputDirectory pointe vers le répertoire dans lequel les fichiers source de type Q seront générés. La valeur de cette propriété sera utile ultérieurement, lorsque vous explorerez les fichiers Q.

Vous devez également ajouter ce répertoire aux dossiers source du projet, si votre IDE ne le fait pas automatiquement - consultez la documentation de votre IDE préféré pour savoir comment faire.

Pour cet article, nous utiliserons un modèle JPA simple de service de blog, composé deUsers et de leursBlogPosts avec une relation un-à-plusieurs entre eux:

@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;

    private String login;

    private Boolean disabled;

    @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "user")
    private Set blogPosts = new HashSet<>(0);

    // getters and setters

}

@Entity
public class BlogPost {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    private String body;

    @ManyToOne
    private User user;

    // getters and setters

}

Pour générer des types Q pour votre modèle, exécutez simplement:

mvn compile

3.2. Explorer les classes générées

Allez maintenant dans le répertoire spécifié dans la propriétéoutputDirectory d'apt-maven-plugin (target/generated-sources/java dans notre exemple). Vous verrez un package et une structure de classe qui reflètent directement votre modèle de domaine, sauf que toutes les classes commencent par la lettre Q (QUser etQBlogPost dans notre cas).

Ouvrez le fichierQUser.java. C'est votre point d'entrée pour créer toutes les requêtes qui ontUser comme entité racine. La première chose que vous remarquerez est l'annotation@Generated qui signifie que ce fichier a été généré automatiquement et ne doit pas être modifié manuellement. Si vous changez l'une de vos classes de modèle de domaine, vous devrez réexécutermvn compile pour régénérer tous les types Q correspondants.

En plus de plusieurs constructeursQUser présents dans ce fichier, vous devez également prendre note d'une instance finale publique statique de la classeQUser:

public static final QUser user = new QUser("user");

Vous pouvez utiliser cette instance dans la plupart des requêtes Querydsl adressées à cette entité, sauf lorsque vous devez écrire des requêtes plus complexes, telles que la jonction de plusieurs instances différentes d'une table dans une même requête.

La dernière chose à noter est que pour chaque champ de la classe d'entité, il existe un champ*Path correspondant dans le type Q, commeNumberPath id,StringPath login etSetPath blogPosts dans la classeQUser (notez que le nom du champ correspondant àSet est au pluriel). Ces champs font partie de l’API de requête courante que nous rencontrerons plus tard.

4. Interroger avec Querydsl

4.1. Interrogation et filtrage simples

Pour créer une requête, nous aurons d'abord besoin d'une instance deJPAQueryFactory, ce qui est une manière préférée de démarrer le processus de construction. La seule chose dontJPAQueryFactory a besoin est unEntityManager, qui devrait déjà être disponible dans votre application JPA via l'appel deEntityManagerFactory.createEntityManager() ou l'injection de@PersistenceContext.

EntityManagerFactory emf =
  Persistence.createEntityManagerFactory("org.example.querydsl.intro");
EntityManager em = entityManagerFactory.createEntityManager();
JPAQueryFactory queryFactory = new JPAQueryFactory(em);

Créons maintenant notre première requête:

QUser user = QUser.user;

User c = queryFactory.selectFrom(user)
  .where(user.login.eq("David"))
  .fetchOne();

Notez que nous avons défini l'utilisateur d'une variable localeQUser et l'avons initialisée avec l'instance statique deQUser.user. Ceci est fait uniquement par souci de concision, sinon vous pouvez importer le champ statiqueQUser.user.

La méthodeselectFrom desJPAQueryFactory commence à créer une requête. Nous lui passons l'instanceQUser et continuons à construire la clause conditionnelle de la requête avec la méthode.where(). Leuser.login est une référence à un champStringPath de la classeQUser que nous avons vu auparavant. L'objetStringPath possède également la méthode.eq() qui permet de continuer à construire couramment la requête en spécifiant la condition d'égalité de champ.

Enfin, pour récupérer la valeur de la base de données dans un contexte de persistance, nous terminons la chaîne de construction par l'appel à la méthodefetchOne(). Cette méthode renvoienull si l’objet est introuvable, mais renvoie unNonUniqueResultException s’il y a plusieurs entités satisfaisant la condition.where().

4.2. Commande et regroupement

Maintenant, allons chercher tous les utilisateurs dans une liste, triés par leur identifiant par ordre d’ascension.

List c = queryFactory.selectFrom(user)
  .orderBy(user.login.asc())
  .fetch();

Cette syntaxe est possible car les classes*Path ont les méthodes.asc() et.desc(). Vous pouvez également spécifier plusieurs arguments pour la méthode.orderBy() pour trier par plusieurs champs.

Essayons maintenant quelque chose de plus difficile. Supposons que nous devions regrouper tous les articles par titre et comptabiliser les titres en double. Ceci est fait avec la clause.groupBy(). Nous voudrons également classer les titres en fonction du nombre d’occurrences.

NumberPath count = Expressions.numberPath(Long.class, "c");

List userTitleCounts = queryFactory.select(
  blogPost.title, blogPost.id.count().as(count))
  .from(blogPost)
  .groupBy(blogPost.title)
  .orderBy(count.desc())
  .fetch();

Nous avons sélectionné le titre de l'article de blog et le nombre de doublons, en les regroupant par titre, puis en les classant par nombre agrégé. Remarquez que nous avons d'abord créé un alias pour le champcount() dans la clause.select(), car nous devions le référencer dans la clause.orderBy().

4.3. Requêtes complexes avec jointures et sous-requêtes

Trouvons tous les utilisateurs ayant écrit un article intitulé "Hello World!". Pour une telle requête, nous pourrions utiliser une jointure interne. Notez que nous avons créé un aliasblogPost pour la table jointe afin de la référencer dans la clause.on():

QBlogPost blogPost = QBlogPost.blogPost;

List users = queryFactory.selectFrom(user)
  .innerJoin(user.blogPosts, blogPost)
  .on(blogPost.title.eq("Hello World!"))
  .fetch();

Essayons maintenant d’obtenir la même chose avec la sous-requête:

List users = queryFactory.selectFrom(user)
  .where(user.id.in(
    JPAExpressions.select(blogPost.user.id)
      .from(blogPost)
      .where(blogPost.title.eq("Hello World!"))))
  .fetch();

Comme nous pouvons le voir, les sous-requêtes sont très similaires aux requêtes, et elles sont également assez lisibles, mais elles commencent par les méthodes d'usine deJPAExpressions. Pour connecter les sous-requêtes à la requête principale, nous référons comme toujours aux alias définis et utilisés précédemment.

4.4. Modifier les données

JPAQueryFactory permet non seulement de créer des requêtes, mais également de modifier et de supprimer des enregistrements. Modifions la connexion de l'utilisateur et désactivons le compte:

queryFactory.update(user)
  .where(user.login.eq("Ash"))
  .set(user.login, "Ash2")
  .set(user.disabled, true)
  .execute();

Nous pouvons avoir n'importe quel nombre de clauses.set() que nous voulons pour différents champs. La clause.where() n'est pas nécessaire, nous pouvons donc mettre à jour tous les enregistrements à la fois.

Pour supprimer les enregistrements correspondant à une certaine condition, nous pouvons utiliser une syntaxe similaire:

queryFactory.delete(user)
  .where(user.login.eq("David"))
  .execute();

La clause.where() n'est pas non plus nécessaire, mais soyez prudent, car l'omission de la clause.where() entraîne la suppression de toutes les entités d'un certain type.

Vous vous demandez peut-être pourquoiJPAQueryFactory n’a pas la méthode.insert(). Il s'agit d'une limitation de l'interface JPA Query. La méthode sous-jacentejavax.persistence.Query.executeUpdate() est capable d'exécuter des instructions de mise à jour et de suppression, mais pas d'insertion. Pour insérer des données, vous devez simplement conserver les entités avec EntityManager.

Si vous souhaitez toujours profiter d'une syntaxe Querydsl similaire pour insérer des données, vous devez utiliser la classeSQLQueryFactory qui réside dans la bibliothèque querydsl-sql.

5. Conclusion

Dans cet article, nous avons découvert une API puissante et sécurisée pour la manipulation d'objet persistant fournie par Querydsl.

Nous avons appris à ajouter Querydsl au projet et avons exploré les types Q générés. Nous avons également traité certains cas d’utilisation typiques et apprécié leur concision et leur lisibilité.

Tout le code source des exemples se trouve dans lesgithub repository.

Enfin, Querydsl propose bien sûr de nombreuses autres fonctionnalités, notamment l'utilisation du SQL brut, des collections non persistantes, des bases de données NoSQL et de la recherche en texte intégral - et nous en explorerons certaines dans les prochains articles.