
В моём учебном проекте на 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