Cartographie dynamique avec Hibernate

1. Introduction

Dans cet article, nous explorerons certaines capacité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 portables vers d'autres implémentations JPA.

2. Configuration du projet

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

 org.hibernate hibernate-core 5.4.12.Final   com.h2database h2 1.4.194 

Pour la version actuelle de la bibliothèque hibernate-core , rendez-vous sur Maven Central.

3. Colonnes calculées avec @Formula

Supposons que nous voulions calculer une valeur de champ d'entité sur la base d'autres propriétés. Une façon de le faire serait de définir un champ en lecture seule calculé 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 que nous devrions faire le recalcul à chaque fois que nous accédons à ce champ virtuel par le getter .

Il serait beaucoup plus facile d'obtenir la valeur déjà calculée à partir 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 SQL que nous avons fourni et insérer les alias de table et de champ corrects. La mise en garde à prendre en compte est que la valeur de l'annotation étant du SQL brut, cela peut rendre notre base de données de mappage dépendante.

Gardez également à l'esprit que la valeur est calculée lorsque l'entité est extraite de la base de données . Par conséquent, lorsque nous persistons ou mettons à jour l'entité, la valeur ne sera pas recalculée jusqu'à ce que l'entité soit 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 des 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 champ booléen .

Nous devrons 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 fournit 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 , puisque nous avons affaire à du SQL brut, la condition @Where ne sera pas réévaluée jusqu'à ce que nous vidions l'entité dans la base de données et l'expulsions du contexte .

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

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é des employés , nous pourrions mapper une collection de téléphones 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 collection Employee.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 qu'elle ne peut pas être désactivée ou activée à la demande.

The @Filter annotation works the same way as @Where, but it also can be enabled or disabled on session level, and also parameterized.

5.1. Defining the @Filter

To demonstrate how @Filter works, let's first add the following filter definition to the Employee entity:

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

The @FilterDef annotation defines the filter name and a set of its parameters that will participate in the query. The type of the parameter is the name of one of the Hibernate types (Type, UserType or CompositeUserType), in our case, an int.

The @FilterDef annotation may be placed either on the type or on package level. Note that it does not specify the filter condition itself (although we could specify the defaultCondition parameter).

This means that we can define the filter (its name and set of parameters) in one place and then define the conditions for the filter in multiple other places differently.

This can be done with the @Filter annotation. In our case, we put it in the same class for simplicity. The syntax of the condition is a raw SQL with parameter names preceded by colons.

5.2. Accessing Filtered Entities

Another difference of @Filter from @Where is that @Filter is not enabled by default. We have to enable it on the session level manually, and provide the parameter values for it:

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

Now suppose we have the following three employees in the database:

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

Then with the filter enabled, as shown above, only two of them will be visible by querying:

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

Note that both the enabled filter and its parameter values are applied only inside the current session. In a new session without filter enabled, we'll see all three employees:

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

Also, when directly fetching the entity by id, the filter is not applied:

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

5.3. @Filter and Second-Level Caching

If we have a high-load application, then we'd definitely want to enable Hibernate second-level cache, which can be a huge performance benefit. We should keep in mind that the @Filter annotation does not play nicely with caching.

The second-level cache only keeps full unfiltered collections. If it wasn't the case, then we could read a collection in one session with filter enabled, and then get the same cached filtered collection in another session even with filter disabled.

This is why the @Filter annotation basically disables caching for the entity.

6. Mapping Any Entity Reference With @Any

Sometimes we want to map a reference to any of multiple entity types, even if they are not based on a single @MappedSuperclass. They could even be mapped to different unrelated tables. We can achieve this with the @Any annotation.

In our example, we'll need to attach some description to every entity in our persistence unit, namely, Employee and Phone. It'd be unreasonable to inherit all entities from a single abstract superclass just to do this.

6.1. Mapping Relation With @Any

Here's how we can define a reference to any entity that implements Serializable (i.e., to any entity at all):

@Entity public class EntityDescription implements Serializable { private String description; @Any( metaDef = "EntityDescriptionMetaDef", metaColumn = @Column(name = "entity_type")) @JoinColumn(name = "entity_id") private Serializable entity; }

The metaDef property is the name of the definition, and metaColumn is the name of the column that will be used to distinguish the entity type (not unlike the discriminator column in the single table hierarchy mapping).

We also specify the column that will reference the id of the entity. It's worth noting that this column will not be a foreign key because it can reference any table that we want.

The entity_id column also can't generally be unique because different tables could have repeated identifiers.

The entity_type/entity_id pair, however, should be unique, as it uniquely describes the entity that we're referring to.

6.2. Defining the @Any Mapping With @AnyMetaDef

Right now, Hibernate does not know how to distinguish different entity types, because we did not specify what the entity_type column could contain.

To make this work, we need to add the meta-definition of the mapping with the @AnyMetaDef annotation. The best place to put it would be the package level, so we could reuse it in other mappings.

Here's how the package-info.java file with the @AnyMetaDef annotation would look like:

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

Ici, nous avons spécifié le type de la colonne entity_type ( chaîne ), le type de la colonne entity_id ( int ), les valeurs acceptables dans la colonne entity_type ( «Employee» et «Phone» ) et les types d'entités correspondants.

Maintenant, supposons que nous ayons un employé avec deux téléphones décrits comme ceci:

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 disponible à l'adresse over sur GitHub.