Dynamisches Mapping mit Ruhezustand

1. Einleitung

In diesem Artikel werden einige dynamische Zuordnungsfunktionen von Hibernate mit den Anmerkungen @Formula , @Where , @Filter und @Any erläutert .

Beachten Sie, dass Hibernate zwar die JPA-Spezifikation implementiert, die hier beschriebenen Anmerkungen jedoch nur in Hibernate verfügbar sind und nicht direkt auf andere JPA-Implementierungen portierbar sind.

2. Projekteinrichtung

Um die Funktionen zu demonstrieren, benötigen wir nur die Hibernate-Core-Bibliothek und eine unterstützende H2-Datenbank:

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

Die aktuelle Version der Hibernate-Core- Bibliothek finden Sie in Maven Central.

3. Berechnete Spalten mit @Formula

Angenommen, wir möchten einen Entitätsfeldwert basierend auf einigen anderen Eigenschaften berechnen. Eine Möglichkeit besteht darin, ein berechnetes schreibgeschütztes Feld in unserer Java-Entität zu definieren:

@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; } }

Der offensichtliche Nachteil ist, dass wir die Neuberechnung jedes Mal durchführen müssen, wenn der Getter auf dieses virtuelle Feld zugreift .

Es wäre viel einfacher, den bereits berechneten Wert aus der Datenbank abzurufen. Dies kann mit der Annotation @Formula erfolgen :

@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; }

Mit @Formula können wir Unterabfragen verwenden, native Datenbankfunktionen und gespeicherte Prozeduren aufrufen und grundsätzlich alles tun, was die Syntax einer SQL-Auswahlklausel für dieses Feld nicht verletzt.

Der Ruhezustand ist intelligent genug, um das von uns bereitgestellte SQL zu analysieren und korrekte Tabellen- und Feldaliasnamen einzufügen. Die Einschränkung ist, dass der Wert der Annotation, da es sich um Raw-SQL handelt, unsere Mapping-Datenbank möglicherweise abhängig macht.

Beachten Sie außerdem, dass der Wert berechnet wird, wenn die Entität aus der Datenbank abgerufen wird . Wenn wir die Entität beibehalten oder aktualisieren, wird der Wert daher erst neu berechnet, wenn die Entität aus dem Kontext entfernt und erneut geladen wird:

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. Entitäten mit @Where filtern

Angenommen, wir möchten der Abfrage eine zusätzliche Bedingung bereitstellen, wenn wir eine Entität anfordern.

Zum Beispiel müssen wir "Soft Delete" implementieren. Dies bedeutet, dass die Entität niemals aus der Datenbank gelöscht wird, sondern nur mit einem booleschen Feld als gelöscht markiert wird .

Wir müssten bei allen vorhandenen und zukünftigen Abfragen in der Anwendung sehr vorsichtig sein. Wir müssten diese zusätzliche Bedingung für jede Abfrage angeben. Glücklicherweise bietet Hibernate eine Möglichkeit, dies an einem Ort zu tun:

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

Die Annotation @Where zu einer Methode enthält eine SQL-Klausel, die jeder Abfrage oder Unterabfrage zu dieser Entität hinzugefügt wird:

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

Wie im Fall der @ Formula- Annotation wird die @ Where- Bedingung , da es sich um unformatiertes SQL handelt, erst neu bewertet, wenn wir die Entität in die Datenbank leeren und sie aus dem Kontext entfernen .

Bis zu diesem Zeitpunkt bleibt die Entität im Kontext und kann mit Abfragen und Suchvorgängen nach ID aufgerufen werden .

Die Annotation @Where kann auch für ein Sammlungsfeld verwendet werden. Angenommen, wir haben eine Liste löschbarer Telefone:

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

Dann könnten wir von der Mitarbeiterseite aus eine Sammlung löschbarer Telefone wie folgt zuordnen :

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

Der Unterschied besteht darin, dass die Employee.phones- Sammlung immer gefiltert wird, wir jedoch weiterhin alle Telefone, einschließlich der gelöschten, per direkter Abfrage erhalten können:

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. Parametrisierte Filterung mit @Filter

Das Problem mit der @ Where- Annotation besteht darin, dass wir nur eine statische Abfrage ohne Parameter angeben können und diese nicht bei Bedarf deaktiviert oder aktiviert werden kann.

Die Annotation @Filter funktioniert genauso wie @Where , kann jedoch auch auf Sitzungsebene aktiviert oder deaktiviert und auch parametrisiert werden.

5.1. Definieren der @filter

Um zu demonstrieren, wie @Filter funktioniert, fügen wir der Employee- Entität zunächst die folgende Filterdefinition hinzu :

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

Die Annotation @FilterDef definiert den Filternamen und eine Reihe seiner Parameter, die an der Abfrage teilnehmen. Der Typ des Parameters ist der Name eines der Hibernate-Typen (Type, UserType oder CompositeUserType), in unserem Fall ein int .

Die Annotation @FilterDef kann entweder auf Typ- oder auf Paketebene platziert werden. Beachten Sie, dass die Filterbedingung selbst nicht angegeben wird (obwohl wir den Parameter defaultCondition angeben könnten ).

Dies bedeutet, dass wir den Filter (seinen Namen und seinen Parametersatz) an einer Stelle definieren und dann die Bedingungen für den Filter an mehreren anderen Stellen unterschiedlich definieren können.

Dies kann mit der Annotation @Filter erfolgen . In unserem Fall haben wir es der Einfachheit halber in dieselbe Klasse eingeordnet. Die Syntax der Bedingung ist eine unformatierte SQL mit Parameternamen, denen Doppelpunkte vorangestellt sind.

5.2. Zugriff auf gefilterte Entitäten

Ein weiterer Unterschied zwischen @Filter und @Where besteht darin, dass @Filter standardmäßig nicht aktiviert ist. Wir müssen es auf Sitzungsebene manuell aktivieren und die Parameterwerte dafür bereitstellen:

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

Angenommen, wir haben die folgenden drei Mitarbeiter in der Datenbank:

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

Wenn dann der Filter aktiviert ist, wie oben gezeigt, werden nur zwei von ihnen durch Abfragen sichtbar:

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

Beachten Sie, dass sowohl der aktivierte Filter als auch seine Parameterwerte nur innerhalb der aktuellen Sitzung angewendet werden. In einer neuen Sitzung ohne aktivierten Filter werden alle drei Mitarbeiter angezeigt:

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

Wenn die Entität direkt anhand der ID abgerufen wird, wird der Filter nicht angewendet:

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

5.3. @ Filter- und Second-Level-Caching

Wenn wir eine Anwendung mit hoher Last haben, möchten wir auf jeden Fall den Cache der zweiten Ebene im Ruhezustand aktivieren, was ein enormer Leistungsvorteil sein kann. Wir sollten bedenken, dass die @ Filter- Annotation beim Caching nicht gut funktioniert.

Der Cache der zweiten Ebene speichert nur vollständige ungefilterte Sammlungen . Wenn dies nicht der Fall wäre, könnten wir eine Sammlung in einer Sitzung mit aktiviertem Filter lesen und dann dieselbe zwischengespeicherte gefilterte Sammlung in einer anderen Sitzung erhalten, auch wenn der Filter deaktiviert ist.

Aus diesem Grund deaktiviert die Annotation @Filter das Caching für die Entität grundsätzlich.

6. Zuordnen einer beliebigen Entitätsreferenz mit @Any

Manchmal möchten wir einen Verweis auf einen von mehreren Entitätstypen zuordnen , auch wenn diese nicht auf einer einzelnen @ MapSuperclass basieren . Sie könnten sogar verschiedenen nicht verwandten Tabellen zugeordnet werden. Dies können wir mit der Annotation @Any erreichen .

In unserem Beispiel müssen wir jeder Entität in unserer Persistenz-Einheit eine Beschreibung hinzufügen , nämlich Mitarbeiter und Telefon . Es wäre unvernünftig, alle Entitäten von einer einzigen abstrakten Oberklasse zu erben, nur um dies zu tun.

6.1. Zuordnungsbeziehung mit @Any

So können wir einen Verweis auf eine Entität definieren, die Serializable implementiert (dh auf eine Entität überhaupt):

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

Hier haben wir den Typ der Spalte entity_type ( Zeichenfolge ), den Typ der Spalte entity_id ( int ), die zulässigen Werte in der Spalte entity_type ( "Mitarbeiter" und "Telefon" ) und die entsprechenden Entitätstypen angegeben.

Angenommen, wir haben einen Mitarbeiter mit zwei Telefonen, die wie folgt beschrieben werden:

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

Jetzt können wir allen drei Entitäten beschreibende Metadaten hinzufügen, obwohl sie unterschiedliche, nicht verwandte Typen haben:

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

In diesem Artikel haben wir einige Anmerkungen von Hibernate untersucht, die eine Feinabstimmung der Entitätszuordnung mithilfe von Raw-SQL ermöglichen.

Der Quellcode für den Artikel ist auf GitHub verfügbar.