Mappatura dinamica con Hibernate

1. Introduzione

In questo articolo, esploreremo alcune funzionalità di mappatura dinamica di Hibernate con le annotazioni @Formula , @Where , @Filter e @Any .

Si noti che sebbene Hibernate implementi la specifica JPA, le annotazioni qui descritte sono disponibili solo in Hibernate e non possono essere trasferite direttamente ad altre implementazioni JPA.

2. Configurazione del progetto

Per dimostrare le funzionalità, avremo solo bisogno della libreria hibernate-core e di un database H2 di supporto:

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

Per la versione corrente della libreria hibernate-core , vai su Maven Central.

3. Colonne calcolate con @Formula

Supponiamo di voler calcolare il valore di un campo entità in base ad altre proprietà. Un modo per farlo sarebbe definire un campo di sola lettura calcolato nella nostra 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'ovvio svantaggio è che dovremmo fare il ricalcolo ogni volta che accediamo a questo campo virtuale dal getter .

Sarebbe molto più semplice ottenere il valore già calcolato dal database. Questo può essere fatto con l' annotazione @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; }

Con @Formula , possiamo usare sottoquery, chiamare funzioni di database native e stored procedure e fondamentalmente fare tutto ciò che non interrompe la sintassi di una clausola di selezione SQL per questo campo.

Hibernate è abbastanza intelligente da analizzare l'SQL fornito e inserire gli alias di tabella e di campo corretti. L'avvertenza di cui essere consapevoli è che poiché il valore dell'annotazione è SQL grezzo, potrebbe rendere dipendente il nostro database di mappatura.

Inoltre, tieni presente che il valore viene calcolato quando l'entità viene recuperata dal database . Quindi, quando persistiamo o aggiorniamo l'entità, il valore non verrà ricalcolato fino a quando l'entità non viene rimossa dal contesto e caricata di nuovo:

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. Filtraggio di entità con @Where

Supponiamo di voler fornire una condizione aggiuntiva alla query ogni volta che richiediamo un'entità.

Ad esempio, dobbiamo implementare la "cancellazione temporanea". Ciò significa che l'entità non viene mai eliminata dal database, ma solo contrassegnata come eliminata con un campo booleano .

Dobbiamo prestare molta attenzione a tutte le query esistenti e future nell'applicazione. Dovremmo fornire questa condizione aggiuntiva a ogni query. Fortunatamente, Hibernate fornisce un modo per farlo in un unico posto:

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

L' annotazione @Where su un metodo contiene una clausola SQL che verrà aggiunta a qualsiasi query o sottoquery a questa entità:

employee.setDeleted(true); session.flush(); session.clear(); employee = session.find(Employee.class, employee.getId()); assertThat(employee).isNull();

Come nel caso dell'annotazione @Formula , poiché abbiamo a che fare con SQL non elaborato , la condizione @Where non verrà rivalutata finché non scarichiamo l'entità nel database e la rimuoviamo dal contesto .

Fino a quel momento, l'entità rimarrà nel contesto e sarà accessibile con query e ricerche per ID .

L' annotazione @Where può essere utilizzata anche per un campo di raccolta. Supponiamo di avere un elenco di telefoni cancellabili:

@Entity public class Phone implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private boolean deleted; private String number; }

Quindi, dal lato dei dipendenti , potremmo mappare una raccolta di telefoni eliminabili come segue:

public class Employee implements Serializable { // ... @OneToMany @JoinColumn(name = "employee_id") @Where(clause = "deleted = false") private Set phones = new HashSet(0); }

La differenza è che la raccolta Employee.phones sarebbe sempre filtrata, ma potremmo comunque ottenere tutti i telefoni, compresi quelli eliminati, tramite query diretta:

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. Filtraggio parametrizzato con @Filter

Il problema con l' annotazione @Where è che ci consente di specificare solo una query statica senza parametri e non può essere disabilitata o abilitata su richiesta.

L' annotazione @Filter funziona allo stesso modo di @Where , ma può anche essere abilitata o disabilitata a livello di sessione e anche parametrizzata.

5.1. Definizione di @Filter

Per dimostrare come funziona @Filter , aggiungiamo prima la seguente definizione di filtro all'entità Employee :

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

L' annotazione @FilterDef definisce il nome del filtro e un insieme dei suoi parametri che parteciperanno alla query. Il tipo del parametro è il nome di uno dei tipi Hibernate (Type, UserType o CompositeUserType), nel nostro caso, un int .

L' annotazione @FilterDef può essere inserita a livello di tipo o di pacchetto. Notare che non specifica la condizione del filtro stesso (sebbene sia possibile specificare il parametro defaultCondition ).

Ciò significa che possiamo definire il filtro (il suo nome e il set di parametri) in un punto e quindi definire le condizioni per il filtro in più altri luoghi in modo diverso.

Questo può essere fatto con l' annotazione @Filter . Nel nostro caso, lo inseriamo nella stessa classe per semplicità. La sintassi della condizione è un SQL grezzo con nomi di parametri preceduti da due punti.

5.2. Accesso alle entità filtrate

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;

Qui abbiamo specificato il tipo della colonna entity_type ( stringa ), il tipo della colonna entity_id ( int ), i valori accettabili nella colonna entity_type ( "Employee" e "Phone" ) ei corrispondenti tipi di entità.

Supponiamo ora di avere un dipendente con due telefoni descritti in questo modo:

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);

Ora potremmo aggiungere metadati descrittivi a tutte e tre le entità, anche se hanno tipi diversi non correlati:

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. Conclusione

In questo articolo, abbiamo esplorato alcune delle annotazioni di Hibernate che consentono di ottimizzare la mappatura delle entità utilizzando SQL grezzo.

Il codice sorgente dell'articolo è disponibile su GitHub.