Статьи

Hibernate и UUID идентификаторы

Вступление

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

Тип базы данных UUID

Существует несколько способов представления 128-битного UUID, и всякий раз, когда у вас возникают сомнения, я обращаюсь к Stack Exchange за советом эксперта.

Поскольку идентификаторы таблиц обычно индексируются, чем компактнее тип базы данных, тем меньше места потребуется для индекса. От самых эффективных до самых маленьких, вот наши варианты:

  1. Некоторые базы данных ( PostgreSQL , SQL Server ) предлагают выделенный тип хранения UUID
  2. В противном случае мы можем хранить биты как байтовый массив (например, RAW (16) в Oracle или стандартный тип BINARY (16))
  3. В качестве альтернативы мы можем использовать 2 больших (64-битных) столбца, но составной идентификатор менее эффективен, чем один столбец
  4. Мы можем сохранить шестнадцатеричное значение в столбце CHAR (36) (например, 32 шестнадцатеричных значения и 4 тире), но это займет больше всего места, следовательно, это наименее эффективная альтернатива

Hibernate предлагает множество стратегий выбора идентификаторов, и для идентификаторов UUID у нас есть три варианта:

  • назначенный генератор сопровождается генерацией логики UUID приложения
  • шестнадцатеричный генератор строк «uuid»
  • более гибкий генератор «uuid2» , позволяющий нам использовать java.lang.UUID , 16-байтовый массив или шестнадцатеричное строковое значение

Назначенный генератор

Назначенный генератор позволяет логике приложения управлять процессом генерации идентификатора объекта. Просто опуская определение генератора идентификаторов, Hibernate рассмотрит назначенный идентификатор. В этом примере используется тип столбца BINARY (16), поскольку целевой базой данных является HSQLDB .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Entity(name = "assignedIdentifier")
public static class AssignedIdentifier {
 
    @Id
    @Column(columnDefinition = "BINARY(16)")
    private UUID uuid;
 
    public AssignedIdentifier() {
    }
 
    public AssignedIdentifier(UUID uuid) {
        this.uuid = uuid;
    }
}

Сохранение сущности:

1
2
session.persist(new AssignedIdentifier(UUID.randomUUID()));
session.flush();

Создает ровно один оператор INSERT:

1
Query:{[insert into assignedIdentifier (uuid) values (?)][[B@76b0f8c3]}

Давайте посмотрим, что происходит при объединении вместо:

1
2
session.merge(new AssignedIdentifier(UUID.randomUUID()));
session.flush();

На этот раз мы получаем и SELECT, и INSERT:

1
2
Query:{[select assignedid0_.uuid as uuid1_0_0_ from assignedIdentifier assignedid0_ where assignedid0_.uuid=?][[B@23e9436c]}
Query:{[insert into assignedIdentifier (uuid) values (?)][[B@2b37d486]}

Метод persist принимает временную сущность и присоединяет ее к текущему сеансу Hibernate. Если есть уже прикрепленный объект или текущий объект отсоединен, мы получим исключение.

Операция слияния скопирует текущее состояние объекта в существующую постоянную сущность (если есть). Эта операция работает как для временных, так и для отдельных объектов, но для временных объектов сохранение намного более эффективно, чем операция слияния.

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

Вот почему метод Spring Data SimpleJpaRepository # save (S entity) не лучший выбор для сущностей, использующих назначенный идентификатор:

1
2
3
4
5
6
7
8
9
@Transactional
public <S extends T> S save(S entity) {
    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

Для назначенных идентификаторов этот метод всегда будет выбирать слияние вместо сохранения, поэтому вы получите и SELECT, и INSERT для каждой вновь вставленной сущности.

Генераторы UUID

На этот раз мы не будем присваивать идентификатор самостоятельно, но Hibernate сгенерирует его от нашего имени. Когда встречается нулевой идентификатор, Hibernate предполагает временную сущность, для которой он генерирует новое значение идентификатора. На этот раз операция слияния не потребует запроса выбора до вставки временного объекта.

UUIDHexGenerator

Шестнадцатеричный генератор UUID является самым старым генератором идентификаторов UUID, и он зарегистрирован под типом «uuid» . Он может генерировать 32 шестнадцатеричное строковое значение UUID (он также может использовать разделитель), имеющий следующий шаблон: 8 {sep} 8 {sep} 4 {sep} 8 {sep} 4.

Этот генератор не соответствует IETF RFC 4122 , в котором используется 8-4-4-4-12-разрядное представление.

1
2
3
4
5
6
7
8
9
@Entity(name = "uuidIdentifier")
public static class UUIDIdentifier {
 
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid")
    @Column(columnDefinition = "CHAR(32)")
    @Id
    private String uuidHex;
}

Сохранение или слияние переходного объекта:

1
2
3
4
session.persist(new UUIDIdentifier());
session.flush();
session.merge(new UUIDIdentifier());
session.flush();

Создает один оператор INSERT для каждой операции:

1
2
Query:{[insert into uuidIdentifier (uuidHex) values (?)][2c929c6646f02fda0146f02fdbfa0000]}
Query:{[insert into uuidIdentifier (uuidHex) values (?)][2c929c6646f02fda0146f02fdbfc0001]}

Вы можете проверить значение строкового параметра, отправленное на запросы SQL INSERT.

UUIDGenerator

Новый генератор UUID совместим с IETF RFC 4122 (вариант 2) и предлагает сменные стратегии генерации. Он зарегистрирован под типом «uuid2» и предлагает более широкий диапазон типов на выбор:

  • java.lang.UUID
  • 16-байтовый массив
  • шестнадцатеричное строковое значение
1
2
3
4
5
6
7
8
9
@Entity(name = "uuid2Identifier")
public static class UUID2Identifier {
 
    @GeneratedValue(generator = "uuid2")
    @GenericGenerator(name = "uuid2", strategy = "uuid2")
    @Column(columnDefinition = "BINARY(16)")
    @Id
    private UUID uuid;
}

Сохранение или слияние переходного объекта:

1
2
3
4
session.persist(new UUID2Identifier());
session.flush();
session.merge(new UUID2Identifier());
session.flush();

Создает один оператор INSERT для каждой операции:

1
2
Query:{[insert into uuid2Identifier (uuid) values (?)][[B@68240bb]}
Query:{[insert into uuid2Identifier (uuid) values (?)][[B@577c3bfa]}

Эти запросы SQL INSERT используют байтовый массив, так как мы настроили определение столбца @Id.

  • Код доступен на GitHub .