Классы в Java могут вступать в наследственные отношения и эти отношения должны как-то сохраняться и при переносе классов в базы данных, в которых наследования, за исключением некоторых реализаций, как бы и нет. JPA предлагает целых четыре решения по заполнению этой пропасти между классами и таблицами.
@MappedSuperclass
Самое простое, что можно придумать, это описать в базовом классе некоторые общие свойства и их отображение в базу данных и потом наследоваться от базового класса, включая это определение.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/**
* Common 'id' part of all entities.
*/
@SuppressWarnings("PMD")
@MappedSuperclass
@ToString
public abstract class AbstractIdentifiableObject {
/**
* Common id field.
*/
@Id
@GeneratedValue
@Getter
@Setter
private Long id;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
/**
* Single financial operation.
*/
@SuppressWarnings("PMD")
@ToString
@Entity
@Table(name = "journal")
public class Operation extends AbstractIdentifiableObject {
/**
* Operation's account.
*/
@Getter
@Setter
@Column(nullable = false, updatable = false)
private Integer accountId;
/**
* Operation's amount.
*/
@Getter
@Setter
@Column(nullable = false, updatable = false, scale = 2, precision = 10)
private BigDecimal amount;
}
|
Аннотация @MappedSuperclass позволяет включать класс и его jpa аннотации в производный класс, не делая базовый класс сущностью. Типичное использование в примере выше — абстрактный базовый класс, несущий в себе суррогатный первичный ключ.
В базе данных всё будет выглядеть, как если бы поля базового класса были определены непосредственно в производном классе.
Одна таблица на все классы
Остальные стратегии JPA рассчитаны на сущности, которые унаследованы от других сущностей. Стратегия по умолчанию в этом случае — хранить все данные базового класса и всех его производных классов в одной таблице. В этой таблице будут созданые столбцы для всех возможных полей всех производных классов и отдельный столбец, в котором будет хранится признак класса, то есть метка, указывающая к какому конкретному классу относится эта строка.
Базовый класс аннотируется аннотацией @Inheritance и, по желанию, @DiscriminatorColumn, в параметрах которой можно задать наименование и тип столбца, в котором будут храниться признаки класса. К производным классам добавляется аннотация @DiscriminatorValue, задающее значение признака класса.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@SuppressWarnings("PMD")
@Entity
@Inheritance
@DiscriminatorColumn
@ToString
public abstract class AbstractNamedObject extends AbstractIdentifiableObject {
/**
* The Name.
*/
@Getter
@Setter
private String name;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
@SuppressWarnings("PMD")
@Entity
@DiscriminatorValue("PRODUCT")
@ToString
public class Product extends AbstractNamedObject {
/**
* Category, to which this product belongs.
*/
@Getter
@Setter
private Long productCategoryId;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
@SuppressWarnings("PMD")
@Entity
@DiscriminatorValue("SERVICE")
@ToString
public class Service extends AbstractNamedObject {
/**
* Service group, to which that service belongs.
*/
@Getter
@Setter
private Long serviceGroup;
}
|
Этот подход хорош тем, что позволяет сравнительно быстро загружать объекты и, при этом, обрабатывать общие поля внешними средствами, без знания о структуре классов. С другой стороны, теряется возможность указывать not null ограничения для столбцов, могут быть проблемы с производительностью при изменении/добавлении строк, если на таблице придётся определять много индексов и некоторые базы данных не очень эффективно работают с длинными строками в таблицах.
По таблице и join’у каждому классу
Другая стратегия — создавать для каждого производного класса свою собственную таблицу, в которой будут храниться его собственные поля. А поля базового класса — в собственной таблице базового класса. В этом случае в аннотации @Inheritance базового класса указывается другая стратегия strategy = InheritanceType.JOINED, а в производных классах указывать ничего не надо.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@SuppressWarnings("PMD")
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@Table(name = "descriptions")
@ToString
public abstract class ObjectWithDescription extends AbstractIdentifiableObject {
/**
* Description text.
*/
@Getter
@Setter
private String description;
}
|
1
2
3
4
5
6
7
8
9
10
11
|
@SuppressWarnings("PMD")
@Entity
@ToString
public class ProductCategory extends ObjectWithDescription {
/**
* Some human friendly category code.
*/
@Getter
@Setter
private String code;
}
|
1
2
3
4
5
6
7
8
9
10
11
|
@SuppressWarnings("PMD")
@Entity
@ToString
public class ServiceGroup extends ObjectWithDescription {
/**
* Some human friendly group code.
*/
@Getter
@Setter
private String code;
}
|
Этот подход решает проблемы предыдущего, с not null ограничениями и возможные переизбытком индексов, но за счёт выполнения сравнительно медленной операции join при чтении данных производного класса из базы. Раздельный доступ к данным базового и производных классов сохраняется.
Раздельные таблицы у каждого класса
И наконец последний вариант, когда каждый класс, и базовый и производные, получают по собственной таблице в которой есть всех их поля, а таблицы не связаны между собой. Для этого у базового класса в аннотации @Inheritance указывается стратегия strategy = InheritanceType.TABLE_PER_CLASS, а у производных классов опять ничего не указывается.
1
2
3
4
5
6
7
8
9
10
11
12
|
@SuppressWarnings("PMD")
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@ToString
public class Cargo extends AbstractIdentifiableObject {
/**
* Cargo weight in kg.
*/
@Getter
@Setter
private Long weight;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@SuppressWarnings("PMD")
@Entity
@ToString
public class LiquidCargo extends Cargo {
/**
* Cargo volume.
*/
@Getter
@Setter
private Long volume;
/**
* Liquid type.
*/
@Getter
@Setter
private String liquidType;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@SuppressWarnings("PMD")
@Entity
@ToString
public class PackedCargo extends Cargo {
/**
* Square box width.
*/
@Getter
@Setter
private Long width;
/**
* Box height.
*/
@Getter
@Setter
private Long height;
}
|
Самое лучше по производительности решение, но теряется возможность обрабатывать данные базовых классов без обхода всех таблиц.
Код примера доступен на github.