«1. Введение

В Hibernate мы можем представить отношения «один ко многим» в наших Java-бинах, задав одно из наших полей в виде списка.

В этом кратком руководстве мы рассмотрим различные способы сделать это с помощью карты.

2. Карты отличаются от списков

Использование карты для представления отношения «один ко многим» отличается от списка, поскольку у нас есть ключ.

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

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

Пара ключ-значение на карте может быть двух типов: тип значения и тип объекта. В следующих разделах мы рассмотрим способы представления этих ассоциаций в Hibernate.

3. Использование @MapKeyColumn

Допустим, у нас есть сущность Order, и мы хотим отслеживать название и цену всех товаров в заказе. Итак, мы хотим ввести Map\u003cString, Double\u003e в Order, который сопоставит имя предмета с его ценой:

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @ElementCollection
    @CollectionTable(name = "order_item_mapping", 
      joinColumns = {@JoinColumn(name = "order_id", referencedColumnName = "id")})
    @MapKeyColumn(name = "item_name")
    @Column(name = "price")
    private Map<String, Double> itemPriceMap;

    // standard getters and setters
}

Нам нужно указать Hibernate, где взять ключ и значение. В качестве ключа мы использовали @MapKeyColumn, указывая, что ключ карты — это столбец item_name нашей таблицы соединения, order_item_mapping. Точно так же @Column указывает, что значение карты соответствует столбцу цены в таблице соединений.

Кроме того, объект itemPriceMap является картой типа значения, поэтому мы должны использовать аннотацию @ElementCollection.

В дополнение к объектам базового типа значения, объекты @Embeddable также могут использоваться в качестве значений карты аналогичным образом.

4. Использование @MapKey

Как мы все знаем, требования со временем меняются — так что теперь, скажем, нам нужно сохранить еще несколько атрибутов Item вместе с itemName и itemPrice:

@Entity
@Table(name = "item")
public class Item {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @Column(name = "name")
    private String itemName;

    @Column(name = "price")
    private double itemPrice;

    @Column(name = "item_type")
    @Enumerated(EnumType.STRING)
    private ItemType itemType;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_on")
    private Date createdOn;
   
    // standard getters and setters
}

Соответственно, давайте изменим Map\u003cString, Double\u003e на Map\u003cString, Item\u003e в классе сущности Order:

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "order_item_mapping", 
      joinColumns = {@JoinColumn(name = "order_id", referencedColumnName = "id")},
      inverseJoinColumns = {@JoinColumn(name = "item_id", referencedColumnName = "id")})
    @MapKey(name = "itemName")
    private Map<String, Item> itemMap;

}

Обратите внимание, что на этот раз мы будем использовать аннотацию @MapKey, чтобы Hibernate использовал Item#itemName в качестве столбца ключа сопоставления вместо введения дополнительного столбца в таблицу соединений. Таким образом, в этом случае таблица соединения order_item_mapping не имеет ключевого столбца — вместо этого он ссылается на имя элемента.

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

Кроме того, itemMap является картой типа объекта, поэтому мы должны аннотировать отношения, используя @OneToMany или @ManyToMany.

5. Использование @MapKeyEnumerated и @MapKeyTemporal

Всякий раз, когда мы указываем перечисление в качестве ключа карты, мы используем @MapKeyEnumerated. Точно так же для временных значений используется @MapKeyTemporal. Поведение очень похоже на стандартные аннотации @Enumerated и @Temporal соответственно.

По умолчанию они аналогичны @MapKeyColumn тем, что ключевой столбец будет создан в таблице соединений. Если мы хотим повторно использовать значение, уже сохраненное в сохраняемом объекте, мы должны дополнительно пометить поле с помощью @MapKey.

6. Использование @MapKeyJoinColumn

Далее, допустим, нам также нужно отслеживать продавца каждого товара. Один из способов сделать это — добавить сущность Продавца и связать ее с сущностью Товара: . Следовательно, давайте изменим Map\u003cString, Item\u003e на Map\u003cSeller, Item\u003e:

@Entity
@Table(name = "seller")
public class Seller {

    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @Column(name = "name")
    private String sellerName;
   
    // standard getters and setters

}
@Entity
@Table(name = "item")
public class Item {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @Column(name = "name")
    private String itemName;

    @Column(name = "price")
    private double itemPrice;

    @Column(name = "item_type")
    @Enumerated(EnumType.STRING)
    private ItemType itemType;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_on")
    private Date createdOn;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "seller_id")
    private Seller seller;
 
    // standard getters and setters
}

Нам нужно добавить @MapKeyJoinColumn, чтобы добиться этого, так как эта аннотация позволяет Hibernate сохранять столбецeller_id (ключ карты) в присоединиться к таблице order_item_mapping вместе со столбцом item_id. Итак, во время чтения данных из базы данных мы можем легко выполнить операцию GROUP BY.

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "order_item_mapping", 
      joinColumns = {@JoinColumn(name = "order_id", referencedColumnName = "id")},
      inverseJoinColumns = {@JoinColumn(name = "item_id", referencedColumnName = "id")})
    @MapKeyJoinColumn(name = "seller_id")
    private Map<Seller, Item> sellerItemMap;

    // standard getters and setters

}

«7. Заключение

В этой статье мы узнали о нескольких способах сохранения Map в Hibernate в зависимости от требуемого сопоставления.

Как всегда, исходный код этого руководства можно найти на Github.

«