Первичные ключи в Hibernate

car-keys-e1426814581598Каждая  сущность должна иметь идентификатор, который её однозначно идентифицирует. В мире SQL подобный идентификатор называется, с некоторыми допущениями,первичный ключ. В качестве такого идентификатора можно использовать примитивные типы и их обёртки, строки, BigDecimal/BigInteger, даты и т.д. Hibernate требует, чтобы каждый такой идентификатор был:

  • уникальным (UNIQUE) — то есть однозначно идентифицировал строку в таблице.
  • Имеющим значение (NOT NULL) — то есть идентификтор не может быть null, а в случае составного идентификатора ни какое из его полей не может быть null.
  • Неизменным (IMMUTABLE) — значение идентификатора определяется при создании записи в базе и в последствии никогда не изменяется.

Естественный ключ или суррогатный ключ

Поле идентификатора помечается аннотацией@Id. Например, если у нас в модели данных есть сущность «Компания» (а я использую модель данных из примера JPQL, в которой эта сущность есть) у которой есть имя компании и имя это соответствует требованиям к идентификаторам, то есть имеет значение, не изменяется и уникально, то это имя можно использовать как идентификатор.

Такой идентификатор называется естественным. Он вытекает непосредственно из модели данных приложения и, обычно, весьма хорошо в неё вписывается. Теория так же говорит нам, что для любой таблицы/сущности можно сформировать естественный ключ.

С другой стороны, естественный ключ может быть и неудобным в использовании. Пример выше не позволяет создать компанию с тем-же самым именем в будущем, когда существующая компания уже прекратит своё существование. Поэтому зачастую используются суррогатные ключи, которые не связаны явно с моделью данных приложения, а либо порождаются из неё, либо создаются каким-либо другим способом. Типичное решение — добавление к сущности дополнительного целочисленного поля.

Суррогатные и натуральные ключи можно использовать параллельно:

В этом случае поле id будет первичным ключом, а name — естественным ключом. С точки зрения базы это будет просто ещё один индекс, а вот Hibernate будет знать, что это тоже идентификатор и будет использовать это знание для оптимизации. Кроме того, по естественному ключу можно делать запросы:

Простые и составные идентификаторы

Все первичные ключи, которые были в примерах выше, являются простыми ключами, то есть состоящими из одного столбца. Но первичный ключ может быть и составным, то есть состоять из более чем одного столбца. Например вы можете решить идентифицировать пользователей по паспорту, то есть серии и номеру. Очевидно, что удобнее было бы хранить серию и номер в разных полях, но тогда гарантировать уникальность значений в каждом столбце не получится и на помощь приходит составной первичный ключ.

Hibernate, разумеется, составные ключи поддерживает, причём двумя разными методами, но требует при это дополнительной работы. В первую очередь необходимо определить класс ключа:

К классу ключа предъявляются некоторые требования:

  • Класс должен быть public
  • У класса должен быть публичный конструктор по умолчанию.
  • Класс должен (корректно) реализовывать собственные equals()  и hashCode()
  • Класс должен реализовывать Serializable

Первый метод использования составного ключа включает класс ключа целиком в класс сущности:

@EmbeddedId указывает на поле составного первичного ключа, а @Embeddable объявляет класс составным ключом.

Второй вариант использования оставляет поля первичного ключа непосредственно в классе сущности, а класс составного ключа служит лишь для поддержки:

Генерируемые автоматически и создаваемые вручную ключи

Значения естественных ключей создаются естественным 🙂 образом при создании экземпляра сущности. А вот суррогатные ключи надо как-то заполнять самому и брать для них откуда-то уникальные значения, что может быть непростым делом в условиях параллельной обработки запросов.

В Hibernate на этот случай предсмотрены механизмы автоматической генерации значений суррогатных ключей, которые включается аннотацией @GeneratedValue.

Когда схема базы данных создаётся Hibernate по описанию сущностей, стратегия генерации значений и необходимая для этой стратегии оснастка создаются автоматически, исходя из возможностей базы данных. Если же схема данных создаётся вручную, то описание таблиц должно совпадает с выбраной стратегией (и поддерживаться базой данных).

Hibernate поддерживает три страгегии генерации значений суррогатного ключа. Первая стратегия, GenerationType.IDENTITY, работает с базами, у которых есть специальные IDENTITY поля, например с MySQL или DB2. В этом случае, для примера выше, таблицу необходимо было бы создавать как:

Вторая стратегия, GenerationType.SEQUENCE, использует встроенный в базы данных, такие как PostgreSQL или Oracle, механизм генерации последовательных значений (sequence). Использование этого генератора требует как создания отдельной sequence в базе данных:

Так и задания имени этой sequence в описании ключа:

Если явно не конфигурировать генератор последовательных значений, HIbernate будет использовать общую последовательность HIBERNATE_SEQUENCE для всех сущностей.

Третья стратегия, GenerationType.TABLE, не зависит от поддержки конкретной базой данных и хранит счётчики значений в отдельной таблице. Hibernate использует таблицу из двух столбцов, для хранения имён последовательностей и текущих значений ключа для генерации:

Параметры табличного генератора могут быть настроены аннотацией @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 достаточно просто:

Hibernate сам переключится на UUID генератор с настройками по умолчанию и будет его использовать. При необходимости можно задать и ручную конфигурацию генератора:

Собственный генератор

Наконец, для тех кому не хватает стандартных генераторов, в Hibernate предусмотрен механизм создания собственных генераторов. Для примера я сделаю целочисленный генератор, который создаёт случайное значение:

Для создания нового генератора необходимо реализовать интерфейс IdentifierGenerator и его метод generate(), который должен вернуть нагенерированное. При необходимости можно использовать текущую сессию и объект, для которого генерируется значение.

Использовать собственный генератор можно с помощью аннотации  @GenericGenerator