Cartographie dynamique avec Hibernate

Cartographie dynamique avec Hibernate

1. introduction

Dans cet article, nous allons explorer certaines fonctionnalités de mappage dynamique d'Hibernate avec les annotations@Formula,@Where,@Filter et@Any.

Notez que bien qu'Hibernate implémente la spécification JPA, les annotations décrites ici ne sont disponibles que dans Hibernate et ne sont pas directement transférables vers d'autres implémentations JPA.

2. Configuration du projet

Pour démontrer les fonctionnalités, nous n'avons besoin que de la bibliothèque hibernate-core et d'une base de données H2 de support:


    org.hibernate
    hibernate-core
    5.2.12.Final


    com.h2database
    h2
    1.4.194

Pour la version actuelle de la bibliothèquehibernate-core, rendez-vous surMaven Central.

3. Colonnes calculées avec@Formula

Supposons que nous voulions calculer une valeur de champ d'entité en fonction de certaines autres propriétés. Une façon de le faire serait de définir un champ calculé en lecture seule dans notre entité Java:

@Entity
public class Employee implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private long grossIncome;

    private int taxInPercents;

    public long getTaxJavaWay() {
        return grossIncome * taxInPercents / 100;
    }

}

L'inconvénient évident est quewe’d have to do the recalculation each time we access this virtual field by the getter.

Il serait beaucoup plus facile d'obtenir la valeur déjà calculée de la base de données. Cela peut être fait avec l'annotation@Formula:

@Entity
public class Employee implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private long grossIncome;

    private int taxInPercents;

    @Formula("grossIncome * taxInPercents / 100")
    private long tax;

}

Avec@Formula, nous pouvons utiliser des sous-requêtes, appeler des fonctions de base de données natives et des procédures stockées et faire tout ce qui ne rompt pas la syntaxe d'une clause de sélection SQL pour ce champ.

Hibernate est suffisamment intelligent pour analyser le code SQL fourni et insérer les alias de table et de champ appropriés. L’avertissement à prendre en compte est que, puisque la valeur de l’annotation est SQL brut, notre base de mappage peut être dépendante de la base de données.

Gardez également à l'esprit quethe value is calculated when the entity is fetched from the database. Par conséquent, lorsque nous persistons ou mettons à jour l'entité, la valeur ne sera pas recalculée tant que l'entité n'aura pas été expulsée du contexte et chargée à nouveau:

Employee employee = new Employee(10_000L, 25);
session.save(employee);

session.flush();
session.clear();

employee = session.get(Employee.class, employee.getId());
assertThat(employee.getTax()).isEqualTo(2_500L);

4. Filtrage d'entités avec@Where

Supposons que nous voulions fournir une condition supplémentaire à la requête chaque fois que nous demandons une entité.

Par exemple, nous devons implémenter la “suppression logicielle”. Cela signifie que l'entité n'est jamais supprimée de la base de données, mais uniquement marquée comme supprimée avec un champboolean.

Nous devons faire très attention à toutes les requêtes existantes et futures dans l'application. Nous devrons fournir cette condition supplémentaire à chaque requête. Heureusement, Hibernate offre un moyen de le faire en un seul endroit:

@Entity
@Where(clause = "deleted = false")
public class Employee implements Serializable {

    // ...
}

L'annotation@Where sur une méthode contient une clause SQL qui sera ajoutée à toute requête ou sous-requête de cette entité:

employee.setDeleted(true);

session.flush();
session.clear();

employee = session.find(Employee.class, employee.getId());
assertThat(employee).isNull();

Comme dans le cas de l'annotation@Formula,since we’re dealing with raw SQL, the @Where condition won’t be reevaluated until we flush the entity to the database and evict it from the context.

Jusque-là, l'entité restera dans le contexte et sera accessible avec des requêtes et des recherches parid.

L'annotation@Where peut également être utilisée pour un champ de collection. Supposons que nous ayons une liste de téléphones supprimables:

@Entity
public class Phone implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private boolean deleted;

    private String number;

}

Ensuite, du côtéEmployee, nous pourrions mapper une collection dephones supprimables comme suit:

public class Employee implements Serializable {

    // ...

    @OneToMany
    @JoinColumn(name = "employee_id")
    @Where(clause = "deleted = false")
    private Set phones = new HashSet<>(0);

}

La différence est que la collectionEmployee.phones serait toujours filtrée, mais nous pourrions toujours obtenir tous les téléphones, y compris ceux supprimés, via une requête directe:

employee.getPhones().iterator().next().setDeleted(true);
session.flush();
session.clear();

employee = session.find(Employee.class, employee.getId());
assertThat(employee.getPhones()).hasSize(1);

List fullPhoneList
  = session.createQuery("from Phone").getResultList();
assertThat(fullPhoneList).hasSize(2);

5. Filtrage paramétré avec@Filter

Le problème avec l'annotation@Where est qu'elle nous permet de spécifier uniquement une requête statique sans paramètres, et elle ne peut pas être désactivée ou activée à la demande.

L'annotation@Filter fonctionne de la même manière que@Where, mais elle peut également être activée ou désactivée au niveau de la session, et également paramétrée.

5.1. Définition des@Filter

Pour illustrer le fonctionnement de@Filter, ajoutons d'abord la définition de filtre suivante à l'entitéEmployee:

@FilterDef(
    name = "incomeLevelFilter",
    parameters = @ParamDef(name = "incomeLimit", type = "int")
)
@Filter(
    name = "incomeLevelFilter",
    condition = "grossIncome > :incomeLimit"
)
public class Employee implements Serializable {

L'annotation@FilterDef définit le nom du filtre et un ensemble de ses paramètres qui participeront à la requête. Le type du paramètre est le nom de l'un des types Hibernate (Type,UserType ouCompositeUserType), dans notre cas, unint.

L'annotationThe @FilterDef peut être placée au niveau du type ou du package. Notez qu'il ne spécifie pas la condition de filtre elle-même (bien que nous puissions spécifier le paramètredefaultCondition).

Cela signifie que nous pouvons définir le filtre (son nom et son ensemble de paramètres) en un seul endroit, puis définir différemment les conditions du filtre à plusieurs autres endroits.

Cela peut être fait avec l'annotation@Filter. Dans notre cas, nous le plaçons dans la même classe pour plus de simplicité. La syntaxe de la condition est un SQL brut avec des noms de paramètres précédés de deux points.

5.2. Accès aux entités filtrées

Une autre différence entre@Filter et@Where est que@Filter n'est pas activé par défaut. Nous devons l'activer manuellement au niveau de la session et lui fournir les valeurs de paramètre suivantes:

session.enableFilter("incomeLevelFilter")
  .setParameter("incomeLimit", 11_000);

Supposons maintenant que nous ayons les trois employés suivants dans la base de données:

session.save(new Employee(10_000, 25));
session.save(new Employee(12_000, 25));
session.save(new Employee(15_000, 25));

Ensuite, avec le filtre activé, comme indiqué ci-dessus, seuls deux d'entre eux seront visibles en interrogeant:

List employees = session.createQuery("from Employee")
  .getResultList();
assertThat(employees).hasSize(2);

Notez que le filtre activé et ses valeurs de paramètre sont appliqués uniquement à l'intérieur de la session en cours. Dans une nouvelle session sans filtre activé, nous verrons les trois employés:

session = HibernateUtil.getSessionFactory().openSession();
employees = session.createQuery("from Employee").getResultList();
assertThat(employees).hasSize(3);

De plus, lors de l'extraction directe de l'entité par identifiant, le filtre n'est pas appliqué:

Employee employee = session.get(Employee.class, 1);
assertThat(employee.getGrossIncome()).isEqualTo(10_000);

5.3. @Filter et mise en cache de deuxième niveau

Si nous avons une application à forte charge, nous souhaitons absolument activer le cache de deuxième niveau Hibernate, ce qui peut être un énorme avantage en termes de performances. Nous devons garder à l'esprit quethe @Filter annotation does not play nicely with caching.

The second-level cache only keeps full unfiltered collections. Si ce n’était pas le cas, nous pourrions lire une collection dans une session avec le filtre activé, puis obtenir la même collection filtrée mise en cache dans une autre session, même avec le filtre désactivé.

C'est pourquoi l'annotation@Filter désactive fondamentalement la mise en cache pour l'entité.

6. Mappage de toute référence d'entité avec@Any

Parfois, nous voulons mapper une référence à l'un des types d'entités multiples, même s'ils ne sont pas basés sur un seul@MappedSuperclass. Ils pourraient même être mappés vers différentes tables non liées. Nous pouvons y parvenir avec l'annotation@Any.

Dans notre exemple,we’ll need to attach some description to every entity in our persistence unit, c'est-à-direEmployee etPhone. Il serait déraisonnable d’hériter de toutes les entités d’une seule superclasse abstraite pour ce faire.

6.1. Relation de mappage avec@Any

Voici comment nous pouvons définir une référence à toute entité qui implémenteSerializable (c'est-à-dire à n'importe quelle entité):

@Entity
public class EntityDescription implements Serializable {

    private String description;

    @Any(
        metaDef = "EntityDescriptionMetaDef",
        metaColumn = @Column(name = "entity_type"))
    @JoinColumn(name = "entity_id")
    private Serializable entity;

}

La propriétémetaDef est le nom de la définition etmetaColumn est le nom de la colonne qui sera utilisée pour distinguer le type d'entité (un peu comme la colonne discriminante dans le mappage de hiérarchie de table unique).

Nous spécifions également la colonne qui référencera lesid de l'entité. Il est intéressant de noter quethis column will not be a foreign key car il peut référencer n'importe quelle table que nous voulons.

La colonneentity_id ne peut généralement pas non plus être unique car différentes tables peuvent avoir des identifiants répétés.

La paireentity_type /entity_id, cependant, doit être unique, car elle décrit de manière unique l'entité à laquelle nous faisons référence.

6.2. Définition du mappage@Any avec@AnyMetaDef

À l'heure actuelle, Hibernate ne sait pas comment distinguer les différents types d'entités, car nous n'avons pas précisé ce que la colonneentity_type pouvait contenir.

Pour que cela fonctionne, nous devons ajouter la méta-définition du mappage avec l'annotation@AnyMetaDef. Le meilleur endroit pour le mettre serait le niveau du paquet, afin que nous puissions le réutiliser dans d'autres mappages.

Voici à quoi ressemblerait le fichierpackage-info.java avec l'annotation@AnyMetaDef:

@AnyMetaDef(
    name = "EntityDescriptionMetaDef",
    metaType = "string",
    idType = "int",
    metaValues = {
        @MetaValue(value = "Employee", targetEntity = Employee.class),
        @MetaValue(value = "Phone", targetEntity = Phone.class)
    }
)
package com.example.hibernate.pojo;

Ici nous avons spécifié le type de la colonneentity_type (string), le type de la colonneentity_id (int), les valeurs acceptables dans leentity_type colonne (“Employee” et“Phone”) et les types d'entités correspondants.

Supposons maintenant que nous ayons un employé avec deux téléphones décrits comme suit:

Employee employee = new Employee();
Phone phone1 = new Phone("555-45-67");
Phone phone2 = new Phone("555-89-01");
employee.getPhones().add(phone1);
employee.getPhones().add(phone2);

Nous pouvons maintenant ajouter des métadonnées descriptives aux trois entités, même si elles ont différents types non liés:

EntityDescription employeeDescription = new EntityDescription(
  "Send to conference next year", employee);
EntityDescription phone1Description = new EntityDescription(
  "Home phone (do not call after 10PM)", phone1);
EntityDescription phone2Description = new EntityDescription(
  "Work phone", phone1);

7. Conclusion

Dans cet article, nous avons exploré certaines des annotations d'Hibernate qui permettent d'ajuster le mappage d'entités à l'aide de SQL brut.

Le code source de l'article est disponibleover on GitHub.