Hibernate Inheritance Mapping

Hibernate Inheritance Mapping

1. Überblick

Relationale Datenbanken haben keine einfache Möglichkeit, Klassenhierarchien auf Datenbanktabellen abzubilden.

Um dieses Problem zu lösen, enthält die JPA-Spezifikation mehrere Strategien:

  • MappedSuperclass - Die übergeordneten Klassen können keine Entitäten sein

  • Einzelne Tabelle - Die Entitäten aus verschiedenen Klassen mit einem gemeinsamen Vorfahren werden in einer einzelnen Tabelle platziert

  • Verknüpfte Tabelle - Jede Klasse verfügt über eine Tabelle, und für die Abfrage einer Unterklassenentität müssen die Tabellen verknüpft werden

  • Tabelle pro Klasse - Alle Eigenschaften einer Klasse befinden sich in ihrer Tabelle, sodass kein Join erforderlich ist

Jede Strategie führt zu einer anderen Datenbankstruktur.

Entitätsvererbung bedeutet, dass wir polymorphe Abfragen zum Abrufen aller Unterklassenentitäten verwenden können, wenn wir nach einer Superklasse abfragen.

Da es sich bei Hibernate um eine JPA-Implementierung handelt, enthält sie alle oben genannten sowie einige Hibernate-spezifische Funktionen im Zusammenhang mit der Vererbung.

In den nächsten Abschnitten werden wir die verfügbaren Strategien genauer erläutern.

2. MappedSuperclass

Bei Verwendung derMappedSuperclass-Strategie ist die Vererbung nur in der Klasse erkennbar, nicht jedoch im Entitätsmodell.

Beginnen wir mit der Erstellung einerPerson-Klasse, die eine übergeordnete Klasse darstellt:

@MappedSuperclass
public class Person {

    @Id
    private long personId;
    private String name;

    // constructor, getters, setters
}

Notice that this class no longer has an @Entity annotation, da es nicht alleine in der Datenbank gespeichert wird.

Als nächstes fügen wir die UnterklasseEmployeehinzu:

@Entity
public class MyEmployee extends Person {
    private String company;
    // constructor, getters, setters
}

In der Datenbank entspricht dies einer“MyEmployee”-Tabelle mit drei Spalten für die deklarierten und geerbten Felder der Unterklasse.

Wenn wir diese Strategie verwenden, können Vorfahren keine Assoziationen mit anderen Entitäten enthalten.

3. Einzelne Tabelle

The Single Table strategy creates one table for each class hierarchy. Dies ist auch die von JPA gewählte Standardstrategie, wenn wir keine explizit angeben.

Wir können die Strategie definieren, die wir verwenden möchten, indem wir der Superklasse die Annotation@Inheritancehinzufügen:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class MyProduct {
    @Id
    private long productId;
    private String name;

    // constructor, getters, setters
}

Die Kennung der Entitäten wird ebenfalls in der Oberklasse definiert.

Dann können wir die Unterklassenentitäten hinzufügen:

@Entity
public class Book extends MyProduct {
    private String author;
}
@Entity
public class Pen extends MyProduct {
    private String color;
}

3.1. Diskriminatorwerte

Da sich die Datensätze für alle Entitäten in derselben Tabelle befinden,Hibernate needs a way to differentiate between them.

By default, this is done through a discriminator column called DTYPE, das den Namen der Entität als Wert hat.

Um die Diskriminatorspalte anzupassen, können wir die Annotation@DiscriminatorColumnverwenden:

@Entity(name="products")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="product_type",
  discriminatorType = DiscriminatorType.INTEGER)
public class MyProduct {
    // ...
}

Hier haben wir uns entschieden,MyProduct Unterklassenentitäten durch eineinteger Spalte mit der Bezeichnungproduct_type. zu unterscheiden

Als Nächstes müssen wir Hibernate mitteilen, welchen Wert jeder Unterklassendatensatz für die Spalteproduct_typehaben wird:

@Entity
@DiscriminatorValue("1")
public class Book extends MyProduct {
    // ...
}
@Entity
@DiscriminatorValue("2")
public class Pen extends MyProduct {
    // ...
}

Im Ruhezustand werden zwei weitere vordefinierte Werte hinzugefügt, die die Anmerkung annehmen kann: "null" und "not null":

  • @DiscriminatorValue(“null”) - bedeutet, dass jede Zeile ohne Diskriminatorwert mit dieser Anmerkung der Entitätsklasse zugeordnet wird. Dies kann auf die Stammklasse der Hierarchie angewendet werden

  • @DiscriminatorValue(“not null”) - Jede Zeile mit einem Diskriminatorwert, der mit keinem der mit Entitätsdefinitionen verknüpften übereinstimmt, wird der Klasse mit dieser Anmerkung zugeordnet

Anstelle einer Spalte können wir auch die Annotation für den Ruhezustand-spezifischen@DiscriminatorFormulaverwenden, um die Differenzierungswerte zu bestimmen:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorFormula("case when author is not null then 1 else 2 end")
public class MyProduct { ... }

This strategy has the advantage of polymorphic query performance since only one table needs to be accessed when querying parent entities. Andererseits bedeutet dies auch, dasswe can no longer use NOT NULL constraints on sub-class Entitätseigenschaften sind.

4. Verbundener Tisch

Using this strategy, each class in the hierarchy is mapped to its table. Die einzige Spalte, die wiederholt in allen Tabellen angezeigt wird, ist die Kennung, die bei Bedarf zum Verknüpfen verwendet wird.

Erstellen wir eine Superklasse, die diese Strategie verwendet:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Animal {
    @Id
    private long animalId;
    private String species;

    // constructor, getters, setters
}

Dann können wir einfach eine Unterklasse definieren:

@Entity
public class Pet extends Animal {
    private String name;

    // constructor, getters, setters
}

Beide Tabellen haben eineanimalId-Kennung. Der Primärschlüssel der EntitätPetunterliegt auch einer Fremdschlüsselbeschränkung für den Primärschlüssel der übergeordneten Entität. Um diese Spalte anzupassen, können Sie die Annotation@PrimaryKeyJoinColumnhinzufügen:

@Entity
@PrimaryKeyJoinColumn(name = "petId")
public class Pet extends Animal {
    // ...
}

The disadvantage of this inheritance mapping method is that retrieving entities requires joins between tables, was bei einer großen Anzahl von Datensätzen zu einer geringeren Leistung führen kann.

Die Anzahl der Verknüpfungen ist höher, wenn die übergeordnete Klasse abgefragt wird, da sie mit jedem einzelnen verwandten untergeordneten Element verknüpft wird. Die Leistung wird also mit höherer Wahrscheinlichkeit beeinträchtigt, je höher die Hierarchie ist, die Datensätze abrufen sollen.

5. Tabelle pro Klasse

Die Strategie "Tabelle pro Klasse" ordnet jede Entität ihrer Tabelle zu, die alle Eigenschaften der Entität enthält, einschließlich der geerbten.

Das resultierende Schema ähnelt dem mit@MappedSuperclass,, aber im Gegensatz dazu definiert die Tabelle pro Klasse tatsächlich Entitäten für übergeordnete Klassen, wodurch Assoziationen und polymorphe Abfragen möglich werden.

Um diese Strategie zu verwenden, müssen wir nur die Annotation@Inheritancezur Basisklasse hinzufügen:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Vehicle {
    @Id
    private long vehicleId;

    private String manufacturer;

    // standard constructor, getters, setters
}

Dann können wir die Unterklassen auf die übliche Weise erstellen.

Dies unterscheidet sich nicht wesentlich von der bloßen Zuordnung jeder Entität ohne Vererbung. Die Unterscheidung wird deutlich, wenn die Basisklasse abgefragt wird, die auch alle Unterklassendatensätze zurückgibt, indem eineUNION-Anweisung im Hintergrund verwendet wird.

The use of UNION can also lead to inferior performance when choosing this strategy. Ein weiteres Problem ist, dass wir die Generierung von Identitätsschlüsseln nicht mehr verwenden können.

6. Polymorphe Abfragen

Wie bereits erwähnt, werden beim Abfragen einer Basisklasse auch alle Unterklassen-Entitäten abgerufen.

Lassen Sie uns dieses Verhalten mit einem JUnit-Test in Aktion sehen:

@Test
public void givenSubclasses_whenQuerySuperclass_thenOk() {
    Book book = new Book(1, "1984", "George Orwell");
    session.save(book);
    Pen pen = new Pen(2, "my pen", "blue");
    session.save(pen);

    assertThat(session.createQuery("from MyProduct")
      .getResultList()).hasSize(2);
}

In diesem Beispiel haben wir zweiBook- undPen-Objekte erstellt und dann deren SuperklasseMyProduct abgefragt, um zu überprüfen, ob zwei Objekte abgerufen werden.

Hibernate kann auch Schnittstellen oder Basisklassen abfragen, die keine Entitäten sind, aber von Entitätsklassen erweitert oder implementiert werden. Sehen wir uns einen JUnit-Test am Beispiel unseres@MappedSuperclassan:

@Test
public void givenSubclasses_whenQueryMappedSuperclass_thenOk() {
    MyEmployee emp = new MyEmployee(1, "john", "example");
    session.save(emp);

    assertThat(session.createQuery(
      "from com.example.hibernate.pojo.inheritance.Person")
      .getResultList())
      .hasSize(1);
}

Beachten Sie, dass dies auch für jede Superklasse oder Schnittstelle funktioniert, unabhängig davon, ob es sich um ein@MappedSuperclasshandelt oder nicht. Der Unterschied zu einer normalen HQL-Abfrage besteht darin, dass wir den vollqualifizierten Namen verwenden müssen, da es sich nicht um Entitäten handelt, die im Ruhezustand verwaltet werden.

Wenn wir nicht möchten, dass eine Unterklasse von diesem Abfragetyp zurückgegeben wird, müssen wir nur die Anmerkung des Ruhezustands@Polymorphismzu ihrer Definition hinzufügen, mit dem TypEXPLICIT:

@Entity
@Polymorphism(type = PolymorphismType.EXPLICIT)
public class Bag implements Item { ...}

In diesem Fall werden bei der Abfrage vonItems, die Datensätze vonBagnicht zurückgegeben.

7. Fazit

In diesem Artikel haben wir die verschiedenen Strategien für die Zuordnung der Vererbung im Ruhezustand gezeigt.

Der vollständige Quellcode der Beispiele istover on GitHub.