Каждая Hibernate сущность должна иметь идентификатор, который её однозначно идентифицирует. В мире SQL подобный идентификатор называется, с некоторыми допущениями,первичный ключ. В качестве такого идентификатора можно использовать примитивные типы и их обёртки, строки, BigDecimal/BigInteger, даты и т.д. Hibernate требует, чтобы каждый такой идентификатор был:
- уникальным (UNIQUE) — то есть однозначно идентифицировал строку в таблице.
- Имеющим значение (NOT NULL) — то есть идентификтор не может быть null, а в случае составного идентификатора ни какое из его полей не может быть null.
- Неизменным (IMMUTABLE) — значение идентификатора определяется при создании записи в базе и в последствии никогда не изменяется.
Естественный ключ или суррогатный ключ
Поле идентификатора помечается аннотацией@Id. Например, если у нас в модели данных есть сущность «Компания» (а я использую модель данных из примера JPQL, в которой эта сущность есть) у которой есть имя компании и имя это соответствует требованиям к идентификаторам, то есть имеет значение, не изменяется и уникально, то это имя можно использовать как идентификатор.
1
2
3
4
5
6
7
8
9
10
11
12
|
@Entity
public class Company {
@Id
@Getter
@Setter
private String name;
@Getter
@Setter
@ManyToMany(mappedBy = "workingPlaces")
private Collection<Person> workers;
}
|
Такой идентификатор называется естественным. Он вытекает непосредственно из модели данных приложения и, обычно, весьма хорошо в неё вписывается. Теория так же говорит нам, что для любой таблицы/сущности можно сформировать естественный ключ.
С другой стороны, естественный ключ может быть и неудобным в использовании. Пример выше не позволяет создать компанию с тем-же самым именем в будущем, когда существующая компания уже прекратит своё существование. Поэтому зачастую используются суррогатные ключи, которые не связаны явно с моделью данных приложения, а либо порождаются из неё, либо создаются каким-либо другим способом. Типичное решение — добавление к сущности дополнительного целочисленного поля.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Entity
public class Company extends AbstractIdentifiableObject {
@Id
@Getter
@Setter
private Long id;
@Getter
@Setter
private String name;
@Getter
@Setter
@ManyToMany(mappedBy = "workingPlaces")
private Collection<Person> workers;
}
|
Суррогатные и натуральные ключи можно использовать параллельно:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Entity
public class Company {
@Id
@GeneratedValue
@Getter
@Setter
private Long id;
@NaturalId
@Getter
@Setter
private String name;
@Getter
@Setter
@ManyToMany(mappedBy = "workingPlaces")
private Collection<Person> workers;
}
|
В этом случае поле id будет первичным ключом, а name — естественным ключом. С точки зрения базы это будет просто ещё один индекс, а вот Hibernate будет знать, что это тоже идентификатор и будет использовать это знание для оптимизации. Кроме того, по естественному ключу можно делать запросы:
1
2
3
4
|
Company c = session
.byNaturalId( Company.class )
.using( "name", "Acme Ltd." )
.load( );
|
Простые и составные идентификаторы
Все первичные ключи, которые были в примерах выше, являются простыми ключами, то есть состоящими из одного столбца. Но первичный ключ может быть и составным, то есть состоять из более чем одного столбца. Например вы можете решить идентифицировать пользователей по паспорту, то есть серии и номеру. Очевидно, что удобнее было бы хранить серию и номер в разных полях, но тогда гарантировать уникальность значений в каждом столбце не получится и на помощь приходит составной первичный ключ.
Hibernate, разумеется, составные ключи поддерживает, причём двумя разными методами, но требует при это дополнительной работы. В первую очередь необходимо определить класс ключа:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@EqualsAndHashCode
@ToString
public class PassportKey implements Serializable {
static final long serialVersionUID = 1L;
@Getter
@Setter
private String series;
@Getter
@Setter
private String n;
}
|
К классу ключа предъявляются некоторые требования:
- Класс должен быть public
- У класса должен быть публичный конструктор по умолчанию.
- Класс должен (корректно) реализовывать собственные equals() и hashCode()
- Класс должен реализовывать Serializable
Первый метод использования составного ключа включает класс ключа целиком в класс сущности:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
@Entity
public class Passport {
@EmbeddedId
private PassportKey key;
@Getter
@Setter
private LocalDate issueDate;
@Getter
@Setter
private Period validity;
@Getter
@Setter
@OneToOne(optional = false, mappedBy = "passport")
private Person owner;
@EqualsAndHashCode
@ToString
@Embeddable
public class PassportKey implements Serializable {
static final long serialVersionUID = 1L;
@Getter
@Setter
private String series;
@Getter
@Setter
private String n;
}
}
|
@EmbeddedId указывает на поле составного первичного ключа, а @Embeddable объявляет класс составным ключом.
Второй вариант использования оставляет поля первичного ключа непосредственно в классе сущности, а класс составного ключа служит лишь для поддержки:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
@Entity
@IdClass(Passport.PassportKey.class)
public class Passport {
@Id
@Getter
@Setter
private String series;
@Id
@Getter
@Setter
private String n;
@Getter
@Setter
private LocalDate issueDate;
@Getter
@Setter
private Period validity;
@Getter
@Setter
@OneToOne(optional = false, mappedBy = "passport")
private Person owner;
@EqualsAndHashCode
@ToString
public class PassportKey implements Serializable {
static final long serialVersionUID = 1L;
@Getter
@Setter
private String series;
@Getter
@Setter
private String n;
}
}
|
Генерируемые автоматически и создаваемые вручную ключи
Значения естественных ключей создаются естественным 🙂 образом при создании экземпляра сущности. А вот суррогатные ключи надо как-то заполнять самому и брать для них откуда-то уникальные значения, что может быть непростым делом в условиях параллельной обработки запросов.
В Hibernate на этот случай предсмотрены механизмы автоматической генерации значений суррогатных ключей, которые включается аннотацией @GeneratedValue.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Entity
public class Company extends AbstractIdentifiableObject {
@Id
@GeneratedValue
@Getter
@Setter
private Long id;
@Getter
@Setter
private String name;
@Getter
@Setter
@ManyToMany(mappedBy = "workingPlaces")
private Collection<Person> workers;
}
|
Когда схема базы данных создаётся Hibernate по описанию сущностей, стратегия генерации значений и необходимая для этой стратегии оснастка создаются автоматически, исходя из возможностей базы данных. Если же схема данных создаётся вручную, то описание таблиц должно совпадает с выбраной стратегией (и поддерживаться базой данных).
Hibernate поддерживает три страгегии генерации значений суррогатного ключа. Первая стратегия, GenerationType.IDENTITY, работает с базами, у которых есть специальные IDENTITY поля, например с MySQL или DB2. В этом случае, для примера выше, таблицу необходимо было бы создавать как:
1
2
3
4
|
CREATE TABLE COMPANY (
ID BIGINT PRIMARY KEY AUTO_INCREMENT
-- Other fields
);
|
Вторая стратегия, GenerationType.SEQUENCE, использует встроенный в базы данных, такие как PostgreSQL или Oracle, механизм генерации последовательных значений (sequence). Использование этого генератора требует как создания отдельной sequence в базе данных:
1
2
3
4
5
6
|
CREATE TABLE COMPANY(
ID BIGINT PRIMARY KEY
-- Other fields
);
CREATE SEQUENCE HIBERNATE_SEQUENCE START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE;
|
Так и задания имени этой sequence в описании ключа:
1
2
3
4
5
6
|
@Id
@SequenceGenerator(name = "hibernateSeq", sequenceName = "HIBERNATE_SEQUENCE")
@GeneratedValue( strategy = GenerationType.SEQUENCE, generator = "hibernateSeq")
@Getter
@Setter
private Long id;
|
Если явно не конфигурировать генератор последовательных значений, HIbernate будет использовать общую последовательность HIBERNATE_SEQUENCE для всех сущностей.
Третья стратегия, GenerationType.TABLE, не зависит от поддержки конкретной базой данных и хранит счётчики значений в отдельной таблице. Hibernate использует таблицу из двух столбцов, для хранения имён последовательностей и текущих значений ключа для генерации:
1
2
3
4
|
create table hibernate_sequences(
sequence_name VARCHAR NOT NULL,
next_val INTEGER NOT NULL
)
|
1
2
3
4
5
|
@Id
@GeneratedValue( strategy = GenerationType.TABLE)
@Getter
@Setter
private Long id;
|
Параметры табличного генератора могут быть настроены аннотацией @TableGenerator, по умолчанию используется таблица HIBERNATE_SEQUENCES и в ней последовательность default
UUID идентификаторы
Помимо обычных типов полей, перечисленных выше, Hibernate поддерживает использование UUID в качестве идентификатора.
UUID (Universally Unique Identifier) — это стандарт идентификации, основное назначение которого, это позволить распределённым системам уникально идентифицировать информацию без центра координации. Таким образом, любой может создать UUID и использовать его для идентификации чего-либо с приемлемым уровнем уверенности, что данный идентификатор непреднамеренно никогда не будет использован для чего-то ещё. Поэтому информация, помеченная с помощью UUID, может быть помещена позже в общую базу данных, без необходимости разрешения конфликта имен.
У использования UUID есть несколько достоинств, по сравнению с обычными автогенерируемыми целочисленными ключами:
- Обеспечивает уникальность идентификаторов не только в пределах одной таблицы, что для некоторых решений может быть важно
- Позволяет генерировать идентификатор записи на клиенте, до сохранения ее в базу
- Делает практически невозможным подбор ключа в случаях, когда запись можно получить, передав ее идентификатор в какой-нибудь публичный API
- Позволяет создавать записи в нескольких репликах базы данных и объединять их в последствии
- Типо-независим
За всё хорошее, с другой стороны, надо платить. В случае UUID ключей платить приходится производительностью:
- Генерация UUID обычно заметно медленнее, чем генерация целочисленного значения
- Выборка из таблиц, связанных друг с другом по UUID, зачастую производится медленнее.
Кроме того, UUID выглядит не очень человекочитаемым, например 452079be-cb27-4ceb-b29f-991e0c31b9e0.
Использование UUID достаточно просто:
1
2
3
4
5
|
@Id
@GeneratedValue
@Getter
@Setter
private UUID id;
|
Hibernate сам переключится на UUID генератор с настройками по умолчанию и будет его использовать. При необходимости можно задать и ручную конфигурацию генератора:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(
name = "uuid",
strategy = "org.hibernate.id.UUIDGenerator",
parameters = {
@Parameter(
name = "uuid_gen_strategy_class",
value = "org.hibernate.id.uuid.CustomVersionOneStrategy"
)
}
)
@Getter
@Setter
private UUID id;
|
Собственный генератор
Наконец, для тех кому не хватает стандартных генераторов, в Hibernate предусмотрен механизм создания собственных генераторов. Для примера я сделаю целочисленный генератор, который создаёт случайное значение:
1
2
3
4
5
6
7
8
9
10
11
12
|
/**
* Generates random INT ids.
*/
public class RandomIdGenerator implements IdentifierGenerator {
private Random r = new Random();
@Override
public Serializable generate(SharedSessionContractImplementor s, Object o)
throws HibernateException {
return r.nextInt();
}
}
|
Для создания нового генератора необходимо реализовать интерфейс IdentifierGenerator и его метод generate(), который должен вернуть нагенерированное. При необходимости можно использовать текущую сессию и объект, для которого генерируется значение.
Использовать собственный генератор можно с помощью аннотации @GenericGenerator
1
2
3
4
5
6
|
@Id
@GenericGenerator(name = "randomGen", strategy = "ru.easyjava.data.hibernate.RandomIdGenerator")
@GeneratedValue(generator = "randomGen")
@Getter
@Setter
private Long id;
|