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

Например, в последние годы приобрели популярность UUID (англ. universally unique identifier — «универсальный уникальный идентификатор»). Основным преимуществом UUID является его (практическая) глобальная уникальность, которая дает огромное преимущество для распределенных систем.

Если используется типичный числовой ID, который увеличивается с каждой новой записью, необходимо сгенерировать все ID одним и тем же системным компонентом. В большинстве случаев это последовательность для каждой таблицы, которая управляется базой данных. Из-за этого последовательность становится единой точкой отказа. Другие подходы, например, кластеризованная база данных или любой другой генератор чисел с горизонтальным масштабированием, требуют связи между узлами. Это, очевидно, создает некоторые трудности, которые замедляют генерацию значений первичного ключа.

При использовании глобально уникального UUID ничего из этого вам не нужно. Каждый компонент может генерировать свои собственные UUID, и конфликтов не будет. Вот почему UUID стали популярными в микросервисной архитектуре и при разработке автономных клиентов.

С другой стороны, UUID имеет и некоторые недостатки. Самый очевидный из них — его размер. Он в 4 раза больше числового ID и не может обрабатываться так же эффективно. Поэтому следует решить, хотите ли вы использовать UUID или числовые идентификаторы, и обсудить этот вопрос с администратором базы данных.

Если вы решите использовать UUID, вы, конечно, также можете хранить их с помощью Hibernate. При этом вам нужно определить, как вы хотите сгенерировать значение UUID. Можно сгенерировать его самостоятельно и установить в объекте-сущности, прежде чем сохранить. Или, если используется Hibernate 4, 5 или 6 или JPA 3.1, вы можете определить стратегию генерации в сопоставлениях сущностей. В этой статье я покажу вам, как это сделать.

Генерация UUID с использованием JPA 3.1

Начиная с JPA 3.1, есть возможность аннотировать атрибут первичного ключа с помощью @GeneratedValue и установить стратегию для GenerationType.UUID. В соответствии со спецификацией, persistence provider должен сгенерировать значение UUID на основе IETF RFC 4122.

@Entity
public class Book {
 
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
     
    …
}

Давайте попробуем маппинг и сохраним новый объект сущности Book.

Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);

В логах можно видеть, что Hibernate сгенерировал значение UUID и установил его в объекте сущности Book, прежде чем сохранить его в базе данных.

18:27:50,009 DEBUG AbstractSaveEventListener:127 - Generated identifier: 21e22474-d31f-4119-8478-d9d448727cfe, using strategy: org.hibernate.id.UUIDGenerator
18:27:50,035 DEBUG SQL:128 - insert into Book (publishingDate, title, version, id) values (?, ?, ?, ?)
18:27:50,039 TRACE bind:28 - binding parameter [1] as [DATE] - [1902-04-30]
18:27:50,040 TRACE bind:28 - binding parameter [2] as [VARCHAR] - [The Hound of the Baskervilles]
18:27:50,040 TRACE bind:28 - binding parameter [3] as [INTEGER] - [0]
18:27:50,040 TRACE bind:28 - binding parameter [4] as [BINARY] - [21e22474-d31f-4119-8478-d9d448727cfe]

Ограничения и проблемы с переносимостью

IETF RFC 4122 определяет 4 различных стратегии создания UUID. Но, к сожалению, JPA 3.1 не указывает, какую версию должен использовать persistence provider. Он также не определяет никакого переносимого механизма для настройки этого процесса генерации.

В связи с этим ваш persistence provider может решить, как он генерирует значения UUID. И это поведение может отличаться в разных реализациях JPA.

Когда Hibernate используется в качестве persistence provider, он генерирует значение UUID на основе случайных чисел, как определено в IETF RFC 4122 версии 4. Я расскажу об этом более подробно, когда буду показывать проприетарные генераторы UUID Hibernate.

Генерация UUID с использованием Hibernate 4, 5 и 6

Как упоминалось ранее, IETF RFC 4122 определяет 4 различных стратегии создания UUID. Hibernate поддерживает две из них:

  1. Стратегия по умолчанию генерирует UUID на основе случайных чисел (IETF RFC 4122, версия 4).

  2. Также можно настроить генератор, который использует IP-адрес компьютера и временную отметку (IETF RFC 4122, версия 1).

Определение стратегии, которую вы хотите использовать, зависит от версии Hibernate. Давайте сначала посмотрим на стратегию по умолчанию.

UUID на основе случайных чисел (IETF RFC 4122, версия 4)

Стратегию генерации на основе случайных чисел Hibernate использует по умолчанию. Также он применяет ее, если используется ранее описанное определение на основе JPA.

UUID на основе случайных чисел в Hibernate 6

Используя Hibernate 6, можно аннотировать атрибут первичного ключа с помощью @UuidGenerator и установить стиль RANDOM, AUTO или вовсе не указывать его. Во всех трех случаях Hibernate будет применять стратегию по умолчанию.

@Entity
public class Book {
     
    @Id
    @GeneratedValue
    @UuidGenerator
    private UUID id;
 
    ...
}

Давайте используем это сопоставление с тестом, как я показал ранее.

Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);

Неудивительно, что это дает те же логи, что и в предыдущем тест-кейсе. Внутри Hibernate использовал тот же стиль, когда я использовал аннотации JPA.

18:28:25,859 DEBUG AbstractSaveEventListener:127 - Generated identifier: ac864ed4-bd3d-4ca0-8ba2-b49ec74465ff, using strategy: org.hibernate.id.uuid.UuidGenerator
18:28:25,879 DEBUG SQL:128 - insert into Book (publishingDate, title, version, id) values (?, ?, ?, ?)
18:28:25,886 TRACE bind:28 - binding parameter [1] as [DATE] - [1902-04-30]
18:28:25,887 TRACE bind:28 - binding parameter [2] as [VARCHAR] - [The Hound of the Baskervilles]
18:28:25,887 TRACE bind:28 - binding parameter [3] as [INTEGER] - [0]
18:28:25,888 TRACE bind:28 - binding parameter [4] as [BINARY] - [ac864ed4-bd3d-4ca0-8ba2-b49ec74465ff]

UUID на основе случайных чисел в Hibernate 4 и 5

При использовании Hibernate 4 или 5 вы можно применять ту же функцию, но нужно приложить немного больше усилий к определению отображения.

К аннотации первичного ключа необходимо добавить атрибут @GeneratedValue. В этой аннотации нужно сослаться на пользовательский генератор и определить этот генератор с помощью аннотации Hibernate @GenericGenerator. Аннотация @GenericGenerator требует два параметра — имя генератора и имя класса, реализующего генератор. В этом случае я назвал генератор «UUID», и Hibernate должен использовать класс org.hibernate.id.UUIDGenerator.

@Entity
public class Book {
 
    @Id
    @GeneratedValue(generator = "UUID")
    @GenericGenerator(
        name = "UUID",
        strategy = "org.hibernate.id.UUIDGenerator",
    )
    private UUID id;
     
    …
}

Это все, что вам нужно сделать, чтобы Hibernate сгенерировал UUID в качестве первичного ключа. Давайте используем этот маппинг, чтобы сохранить новый объект сущности Book.

Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);

Как можно увидеть в логах, Hibernate генерирует UUID и устанавливает его в качестве значения id перед записью новой записи в базу данных.

12:23:19,356 DEBUG AbstractSaveEventListener:118 – Generated identifier: d7cd23b8-991c-470f-ac63-d8fb106f391e, using strategy: org.hibernate.id.UUIDGenerator
12:23:19,388 DEBUG SQL:92 – insert into Book (publishingDate, title, version, id) values (?, ?, ?, ?)
12:23:19,392 TRACE BasicBinder:65 – binding parameter [1] as [DATE] – [1902-04-30]
12:23:19,393 TRACE BasicBinder:65 – binding parameter [2] as [VARCHAR] – [The Hound of the Baskervilles]
12:23:19,393 TRACE BasicBinder:65 – binding parameter [3] as [INTEGER] – [0]
12:23:19,394 TRACE BasicBinder:65 – binding parameter [4] as [OTHER] – [d7cd23b8-991c-470f-ac63-d8fb106f391e]

UUID на основе IP и временной отметки (IETF RFC 4122, версия 1)

Hibernate также может генерировать UUID на основе IETF RFC 4122 версии 1. В соответствии со спецификацией, вместо IP-адреса нужно генерировать UUID с MAC-адресом. MAC-адрес каждого устройства должен быть уникальным, что поможет создать уникальный UUID.

Hibernate использует IP-адрес вместо MAC-адреса. Вообще говоря, это не проблема. Однако, если серверы распределенной системы работают в разных сетях, необходимо убедиться, что ни один из них не использует один и тот же IP-адрес.

Конфигурация генератора UUID на основе IETF RFC 4122 версии 1 очень похожа на предыдущую.

UUID на основе IP и временной метки в Hibernate 6

Аннотация @UuidGenerator, представленная в Hibernate 6, имеет атрибут стиля, который можно использовать для определения того, как Hibernate будет генерировать значение UUID. Когда вы устанавливаете его на TIME, он использует временную метку и IP-адрес для генерации значения UUID.

@Entity
public class Book {
     
    @Id
    @GeneratedValue
    @UuidGenerator(style = Style.TIME)
    private UUID id;
 
    ...
}

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

Давайте используем маппинг, чтобы сохранить новый объект сущности Book.

Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);

Как видите, логи похожи на предыдущие выполнения теста. Hibernate генерирует новое значение UUID и использует его для установки атрибута id перед сохранением новой записи в таблице Book.

18:28:57,068 DEBUG AbstractSaveEventListener:127 - Generated identifier: c0a8b235-8207-1771-8182-07d7756a0000, using strategy: org.hibernate.id.uuid.UuidGenerator
18:28:57,095 DEBUG SQL:128 - insert into Book (publishingDate, title, version, id) values (?, ?, ?, ?)
18:28:57,101 TRACE bind:28 - binding parameter [1] as [DATE] - [1902-04-30]
18:28:57,101 TRACE bind:28 - binding parameter [2] as [VARCHAR] - [The Hound of the Baskervilles]
18:28:57,102 TRACE bind:28 - binding parameter [3] as [INTEGER] - [0]
18:28:57,102 TRACE bind:28 - binding parameter [4] as [BINARY] - [c0a8b235-8207-1771-8182-07d7756a0000]

UUID на основе IP и временной метки в Hibernate 4 и 5

Если используется Hibernate 4 или 5, нужно установить дополнительный параметр в аннотации @GenericGenerator, чтобы определить стратегию генерации. Пример дан в следующем фрагменте кода.

Вы определяете стратегию, предоставляя аннотацию @Parameter с именем uuid_gen_strategy_class и полным именем класса стратегии генерации в качестве значения.

@Entity
public class Book {
 
    @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"
            )
        }
    )
    @Column(name = "id", updatable = false, nullable = false)
    private UUID id;
     
    …
}

Теперь, когда вы сохраните новый объект Book, Hibernate будет использовать класс CustomVersionOneStrategy для создания UUID на основе IETF RFC 4122 версии 1.

Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);

Как видно из логов, Hibernate использует обе стратегии одинаково.

12:35:22,760 DEBUG AbstractSaveEventListener:118 – Generated identifier: c0a8b214-578f-131a-8157-8f431d060000, using strategy: org.hibernate.id.UUIDGenerator
12:35:22,792 DEBUG SQL:92 – insert into Book (publishingDate, title, version, id) values (?, ?, ?, ?)
12:35:22,795 TRACE BasicBinder:65 – binding parameter [1] as [DATE] – [1902-04-30]
12:35:22,795 TRACE BasicBinder:65 – binding parameter [2] as [VARCHAR] – [The Hound of the Baskervilles]
12:35:22,796 TRACE BasicBinder:65 – binding parameter [3] as [INTEGER] – [0]
12:35:22,797 TRACE BasicBinder:65 – binding parameter [4] as [OTHER] – [c0a8b214-578f-131a-8157-8f431d060000]

Подведем итоги

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

JPA 3.1 добавляет значение UUID в enum GenerationType и требует, чтобы persistence provider генерировал UUID на основе IETF RFC 4122. Но он не определяет, какой из четырех подходов следует использовать, и не предоставляет переносимого способа настройки генерации UUID.

Уже несколько лет Hibernate может генерировать значения UUID. В версиях 4 и 5 нужно использовать @GenericGenerator и указывать класс используемого генератора. Hibernate 6 упростил это, введя для него аннотацию @UuidGenerator


Приглашаем на открытое занятие «Кто такие дженерики?», на котором изучим, для чего они нужны и как их начать использовать. Регистрация по ссылке.

Комментарии (1)


  1. MaratGat
    05.09.2022 14:19

    Спасибо за статью. Допустим мы навесили в базе индекс уникальности ключа, будет ли хабернейт автоматически перегенерировать ключ