Каждая 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; |