Картографиране на наследяването в хибернация

1. Общ преглед

Релационните бази данни нямат ясен начин за картографиране на йерархии на класове върху таблици на бази данни.

За да се справи с това, спецификацията JPA предоставя няколко стратегии:

  • MappedSuperclass - родителските класове, не могат да бъдат обекти
  • Единична таблица - обектите от различни класове с общ предшественик се поставят в една таблица
  • Обединена таблица - всеки клас има своя таблица и заявка за обект на подклас изисква присъединяване към таблиците
  • Table-Per-Class - всички свойства на клас са в неговата таблица, така че не е необходимо присъединяване

Всяка стратегия води до различна структура на базата данни.

Унаследяването на обект означава, че можем да използваме полиморфни заявки за извличане на всички обекти от подкласа, когато заявяваме за супер-клас.

Тъй като Hibernate е изпълнение на JPA, той съдържа всичко по-горе, както и няколко специфични за Hibernate функции, свързани с наследяването.

В следващите раздели ще разгледаме по-подробно наличните стратегии.

2. MappedSuperclass

Използвайки стратегията MappedSuperclass , наследяването е очевидно само в класа, но не и модела на обекта.

Нека започнем със създаването на клас Person, който ще представлява родителски клас:

@MappedSuperclass public class Person { @Id private long personId; private String name; // constructor, getters, setters }

Забележете, че този клас вече няма анотация @Entity , тъй като няма да се запазва в базата данни сам по себе си.

След това нека добавим подклас на служител :

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

В базата данни това ще съответства на една таблица „MyEfficiee“ с три колони за декларираните и наследени полета на подкласа.

Ако използваме тази стратегия, предците не могат да съдържат асоциации с други обекти.

3. Единична маса

Стратегията за единична таблица създава по една таблица за всяка йерархия на класове. Това е и стратегията по подразбиране, избрана от JPA, ако не я посочим изрично.

Можем да дефинираме стратегията, която искаме да използваме, като добавим анотацията @Inheritance към супер-класа:

@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) public class MyProduct { @Id private long productId; private String name; // constructor, getters, setters }

Идентификаторът на обектите също е дефиниран в супер-класа.

След това можем да добавим обекти от подкласа:

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

3.1. Дискриминационни ценности

Тъй като записите за всички обекти ще бъдат в една и съща таблица, Hibernate се нуждае от начин за разграничаване между тях.

По подразбиране това става чрез дискриминаторна колона, наречена DTYPE, която има името на обекта като стойност.

За да персонализираме колоната дискриминатор, можем да използваме анотацията @DiscriminatorColumn :

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

Тук избрахме да разграничим обектите на MyProduct от подкласа чрез цяло число колона, наречена product_type .

След това трябва да кажем на Hibernate каква стойност ще има всеки запис от подклас за колоната product_type :

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

Hibernate добавя две други предварително дефинирани стойности, които анотацията може да приеме: „ null “ и „ not null “:

  • @DiscriminatorValue (“null”) - означава, че всеки ред без стойност на дискриминатор ще бъде преобразуван в класа на обекта с тази анотация; това може да се приложи към основния клас на йерархията
  • @DiscriminatorValue („не е нула“) - всеки ред със стойност на дискриминатор, която не съответства на нито една от свързаните с дефиниции на обекти, ще бъде картографиран в класа с тази анотация

Вместо колона можем да използваме специфичната за Hibernate анотация @DiscriminatorFormula, за да определим диференциращите стойности:

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

Тази стратегия има предимството на полиморфната производителност на заявката, тъй като при заявките на родителски обекти трябва да се има достъп само до една таблица. От друга страна, това също означава, че вече не можем да използваме NOT NULL ограничения върху свойствата на обекта от подклас .

4. Обединена маса

Използвайки тази стратегия, всеки клас в йерархията се съпоставя със своята таблица. Единствената колона, която се появява многократно във всички таблици, е идентификаторът, който ще се използва за присъединяването им, когато е необходимо.

Нека създадем суперклас, който използва тази стратегия:

@Entity @Inheritance(strategy = InheritanceType.JOINED) public class Animal { @Id private long animalId; private String species; // constructor, getters, setters }

След това можем просто да дефинираме подклас:

@Entity public class Pet extends Animal { private String name; // constructor, getters, setters }

Both tables will have an animalId identifier column. The primary key of the Pet entity also has a foreign key constraint to the primary key of its parent entity. To customize this column, we can add the @PrimaryKeyJoinColumn annotation:

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

The disadvantage of this inheritance mapping method is that retrieving entities requires joins between tables, which can result in lower performance for large numbers of records.

The number of joins is higher when querying the parent class as it will join with every single related child – so performance is more likely to be affected the higher up the hierarchy we want to retrieve records.

5. Table per Class

The Table Per Class strategy maps each entity to its table which contains all the properties of the entity, including the ones inherited.

The resulting schema is similar to the one using @MappedSuperclass, but unlike it, table per class will indeed define entities for parent classes, allowing associations and polymorphic queries as a result.

To use this strategy, we only need to add the @Inheritance annotation to the base class:

@Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public class Vehicle { @Id private long vehicleId; private String manufacturer; // standard constructor, getters, setters }

Then, we can create the sub-classes in the standard way.

This is not very different from merely mapping each entity without inheritance. The distinction is apparent when querying the base class, which will return all the sub-class records as well by using a UNION statement in the background.

The use of UNION can also lead to inferior performance when choosing this strategy. Another issue is that we can no longer use identity key generation.

6. Polymorphic Queries

As mentioned, querying a base class will retrieve all the sub-class entities as well.

Let's see this behavior in action with a JUnit test:

@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 this example, we've created two Book and Pen objects, then queried their super-class MyProduct to verify that we'll retrieve two objects.

Hibernate can also query interfaces or base classes which are not entities but are extended or implemented by entity classes. Let's see a JUnit test using our @MappedSuperclass example:

@Test public void givenSubclasses_whenQueryMappedSuperclass_thenOk() { MyEmployee emp = new MyEmployee(1, "john", "baeldung"); session.save(emp); assertThat(session.createQuery( "from com.baeldung.hibernate.pojo.inheritance.Person") .getResultList()) .hasSize(1); }

Имайте предвид, че това работи и за всеки супер клас или интерфейс, независимо дали е @MappedSuperclass или не. Разликата от обичайната HQL заявка е, че трябва да използваме напълно квалифицираното име, тъй като те не са управлявани от Hibernate обекти.

Ако не искаме да се връща подклас от този тип заявка, тогава трябва само да добавим анотацията Hibernate @Polymorphism към нейната дефиниция с тип EXPLICIT :

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

В този случай при запитване за артикули записите на чантата няма да бъдат върнати.

7. Заключение

В тази статия показахме различните стратегии за картографиране на наследяването в Hibernate.

Пълният изходен код на примерите може да бъде намерен в GitHub.