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

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

  • Естественным или суррогатным.
  • Генерируемым автоматически или создаваемым вручную.
  • Простым или составным.

Поля, которые образуют идентификатор обычно помечаются аннотацией @Id.

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

Естественный ключ формируется из полей таблицы, которые идентифицируют запись естественным путём ???? Например, если у нас будут пользователи, которые идентифицируются по имени пользователя, это имя и будет естественным первичным ключом:

/**
* User definition.
*/
@SuppressWarnings("PMD")
@ToString
@Entity
@Table(name = "users")
public class User {


    /**
     * User login name.
     */
    @Id
    @Getter
    @Setter
    private String username;


    /**
     * User password.
     */
    @Getter
    @Setter
    @Column(nullable = false)
    private String password;


    /**
     * User e-mail, must be unique.
     */
    @Getter
    @Setter
    @Column(unique = true, nullable = false)
    private String email;
}

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

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

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

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

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

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

/**
* Operation id.
*/
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Getter
@Setter
@Column(name = "id", nullable = false, updatable = false)
private Long rowId;

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

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

CREATE TABLE JOURNAL (
  ID BIGINT PRIMARY KEY AUTO_INCREMENT
);

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

CREATE TABLE JOURNAL (
  ID BIGINT PRIMARY KEY
);


CREATE SEQUENCE JPA_SEQUENCE START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE;

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

@Id
@SequenceGenerator( name = "jpaSequence", sequenceName = "JPA_SEQUENCE", allocationSize = 1, initialValue = 1 )
@GeneratedValue( strategy = GenerationType.SEQUENCE, generator = "jpaSequence")
@Getter
@Setter
@Column(name = "id", nullable = false, updatable = false)
private Long rowId;

В зависимости от требований приложения, можно иметь один генератор на всё приложение, по генератору на каждую сущность или несколько генераторов, которыми пользуются несколько сущностей.

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

CREATE TABLE JOURNAL
(
    ID BIGINT PRIMARY KEY
);


CREATE TABLE SEQ_STORE
(
SEQ_NAMEVARCHAR(255) PRIMARY KEY,
SEQ_VALUEBIGINT NOT NULL
);


INSERT INTO SEQ_STORE VALUES ('JOURNAL.ID.PK', 0);

Затем создать генератор и связать его со идентификатором:

@Id
@TableGenerator( name = "seqStore", table = "SEQ_STORE", pkColumnName = "SEQ_NAME", pkColumnValue = "JOURNAL.ID.PK", valueColumnName = "SEQ_VALUE", initialValue = 1, allocationSize = 1 )
@GeneratedValue( strategy = GenerationType.TABLE, generator = "seqStore" )
@Getter
@Setter
@Column(name = "id", nullable = false, updatable = false)
private Long rowId;

@TableGenerator принимает кучу аргументов:

  • name — имя этого конкретного генератора
  • table — имя таблицы, в которой он хранит текущие значения ключей
  • pkColumnName — имя столбца, в котором хранятся имена значений ключей
  • pkColumnValue — имя ключа, используемого в этом генераторе
  • valueColumnName — имя столбца, в котором хранятся значения ключей

Простые и составные первичные ключи

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

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

@EqualsAndHashCode
@ToString
public class CustomerKey implements Serializable {


    static final long serialVersionUID = 1L;


    @Getter
    @Setter
    private String passportSeries;


    @Getter
    @Setter
    private String passportSNo;


}

Класс ключа должен отвечать требованиям JPA:

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

Класс составного первичного ключа может быть связан с классом сущности двумя разными методами. Один подход заключается в добавлении соответствующих полей в класс сущности и назначение сущности класса составного первичного ключа с помощью аннотации @IdClass. Кроме того, поля, входящие в составной первичный ключ, тоже следует обозначить аннотацией @Id

/**
* Customer definition.
*/
@SuppressWarnings("PMD")
@ToString
@Entity
@Table(name = "customers")
@IdClass(CustomerKey.class)
public class Customer {


    /**
     * Customer's passport series value.
     */
    @Id
    @Getter
    @Setter
    private String passportSeries;


    /**
     * Customer's password number value.
     */
    @Id
    @Getter
    @Setter
    private String passportSNo;


    /**
     * Customer name.
     */
    @Getter
    @Setter
    private String name;
}

Второй вариант, это включение класса составного первичного ключа непосредственно в класс сущности:

/**
* Customer definition.
*/
@SuppressWarnings("PMD")
@ToString
@Entity
@Table(name = "customers")
@IdClass(CustomerKey.class)
public class Customer {
    /**
     * Customer's passport data.
     */
    @EmbeddedId
    @Getter
    @Setter
    private CustomerKey passport;
    
    /**
     * Customer name.
     */
    @Getter
    @Setter
    private String name;
}

В этом случае класс составного первичного ключа дополнительно аннотируется аннотацией @Embeddable и добавляется непосредственно с аннотацией @EmbeddedId в класс сущности.

Вариантов ключей много, какой же выбрать?

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

Скачать код примера

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *