Составной первичный ключ в JPA/ Hibernate

Мои ключи. Инкапсулированные в клетке.

В моём учебном проекте на Java по изучению Spring и RESTful сервисов я столкнулся с небольшой проблемой: в одном из классов был составной первичный ключ, и мне нужно было рассказать Hibernate, что сразу несколько полей соответствующего класса должны сопоставляться с первичным ключом. Для этого пришлось немного погуглить. И то, как это сделать, я расскажу ниже не примере своего проекта.

Итак, в моём проекте – музыкальном интернет-магазине кроме альбомов, артистов и их релизов в базе данных хранится, естественно, ещё и информация о пользователях и их заказах. В системе будут заказы, а в заказах может быть по несколько позиций – каждая их позиций уже описана в музыкальном каталоге (о нём и отношениях один-ко-многим я уже начал писать в этой статье) .

На диаграмме показаны отношения между таблицами ORDER_INFO, в которой описаны заказы, RELEASE, где хранятся релизы – то есть представленные на витрине пластинки и диски, и ORDER_ITEM, которая является промежуточной и хранит информацию о позициях в заказах. Именно в ней и будет составной (сложный) первичный ключ.

Таблица с заказами называется ORDER_INFO, так как слово ORDER зарезервировано для сортировки.

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

Составной первичный ключ у меня получился в таблице с содержимым заказа: одно поле соответствует релизу из каталога, а другое – заказу. Логично, что сочетание заказ-релиз уникально. В случае, если в заказе оказывается несколько одинаковых релизов (например, клиент покупает две пластинки: одну себе в коллекцию, а другую – другу), то мы просто увеличиваем поле quantity.

Итак, код класса, соответствующего этой сущности получается довольно простым и коротким (спасибо скажем lombok и аннотации @Data, которая позволяет скрывать геттеры и сеттеры и конструкторы):

@Entity
@Data
public class OrderItem {

    private Order order;

    private Release release;

    private int quantity;

    private OrderItemStatus status;

}
 

Теперь пришло время обозначить первичный ключ. Это можно сделать двумя способами: с помощью аннотации @IdClass или @Embeddable. И в том и в другом случае для первичного ключа нужно создать отдельный класс, в который требуется поместить оба поля и который должен реализовать интерфейс Serializable. Я называю свой класс OrderItemPK, и выглядит он пока что следующим образом:


public class OrderItemPK implements Serializable {

    private Order order;

    private Release release;

    //убрал для краткости геттеры, сеттеры
}

Не забудьте также переопределить методы equals(Object o) и hash().

Составной первичный ключ и @IdClass

Если вы выбираете аннотацию @IdClass, то разместить её нужно перед классом, в котором первичный ключ находится – то есть в моём случае перед OrderItem. В скобках необходимо указать имя класса с самим ключом, то есть OrderItemPK. При этом оба поля дублируются: они оказываются и в классе-ключе и в классе-сущности, каждый с привычной аннотацией

@Entity
@Data
@IdClass(OrderItemPK.class)
public class OrderItem {

    @Id.
    private Order order;

    @Id.
    private Release release;

    private int quantity;

    private OrderItemStatus status;

}

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

Составной первичный ключ и @EmbeddedId

Я предпочёл альтернативный вариант. В этом случае класс с сущностью мы подписываем @Embeddable, и нам не приходится дублировать поля. Вместо полей, которые выходят в составной ключ, мы просто помещаем объект типа OrderItemPK с аннотацией @EmbeddedId вместо привычного @Id. Получаются такие классы:.

@Embeddable
public class OrderItemPK implements Serializable {

    private Order order;

    private Release release;

}
@Entity
@Data
public class OrderItem {

    @EmbeddedId
    private OrderItemPK pk;

    private int quantity;

    private OrderItemStatus status;

}

Кроме отсутствии дублирования этот подход ещё отличается тем, что в HQL мы обращаемся к отдельным составляющим первичного ключа немного длиннее: select o.OrderItemPK.release from OrderItem o. Но это не беда, зато можно первичный ключ использовать, как полноценный цельный объект.

Код классов с диаграмм с отношением @OneToMany

Напоследок выложу все три класса, соответствующие сущностям с диаграммы, которую я поместил в начале статьи.

Про отношения один-ко-многим я уже говорил, поэтому не буду на них заострять внимание, Каждый заказ также связан с клиентом, но клиентов в этой статье я оставляю за кулисами – от них осталось только поле client с аннотацией @ManyToOne.

Интереснее в этом случае связь @OneToMany с заветной таблицей OrderItem: в атрибуте mappedBy указываем поле pk.order, так как теперь order это поле объекта pk в классе OrderItem.

@Table(name = "ORDER_INFO")
@Entity
@Data
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long orderId;

    @ManyToOne
    @JoinColumn(name="CLIENT_ID", nullable = false)
    private Client client;

    private OrderStatus status;

    @OneToMany(mappedBy = "pk.order",
            fetch = FetchType.LAZY,
            cascade = CascadeType.ALL)
    List<OrderItem> orderItems;
}
@Table(name = "ORDER_ITEM")
@Entity
@Data
public class OrderItem {

    @EmbeddedId
    private OrderItemPK pk;

    private int quantity;

    private OrderItemStatus status;

}
@Embeddable
public class OrderItemPK implements Serializable {

    @ManyToOne
    @JoinColumn(name = "ORDER_ID")
    @JsonIgnore
    private Order order;

    @ManyToOne
    @JoinColumn(name = "RELEASE_ID")
    private Release release;

}
@Table(name="RELEASE")
@Entity
@Data
public class Release {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long releaseId;

    @ManyToOne
    @JoinColumn(name="ALBUM_ID")
    @JsonIgnore
    private Album album;

    private Date releaseDate;

    private String format;

    private String notes;

    private String label;

    private int price;

    private String img;
}

На сегодня на этом всё. В следующий раз продолжу рассказывать про этот проект – про Контроллеры и Сервисы.

4 Comments

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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s