«1. Введение
В этом кратком руководстве по Hibernate мы рассмотрим пример отображения «один ко многим» с использованием аннотаций JPA, альтернативы XML.
Мы также узнаем, что такое двунаправленные отношения, как они могут создавать несоответствия и как может помочь идея владения.
2. Описание
Проще говоря, сопоставление «один ко многим» означает, что одна строка в таблице сопоставляется с несколькими строками в другой таблице.
Давайте посмотрим на следующую диаграмму отношений сущностей, чтобы увидеть ассоциацию «один ко многим»:
В этом примере мы реализуем систему корзин, где у нас есть таблица для каждой тележки и другая таблица для каждый предмет. В одной корзине может быть много товаров, поэтому здесь у нас есть отображение «один ко многим».
Это работает на уровне базы данных: у нас есть cart_id в качестве первичного ключа в таблице корзины, а также cart_id в качестве внешнего ключа в элементах.
Мы делаем это в коде с помощью @OneToMany.
Давайте сопоставим класс Cart с объектом Items таким образом, чтобы отразить отношения в базе данных:
public class Cart {
//...
@OneToMany(mappedBy="cart")
private Set<Items> items;
//...
}
Мы также можем добавить ссылку на Cart в Items, используя @ManyToOne, сделав эту связь двунаправленной. Двунаправленность означает, что мы можем получить доступ к товарам из корзин, а также к тележкам из товаров.
Свойство mappedBy — это то, что мы используем, чтобы сообщить Hibernate, какую переменную мы используем для представления родительского класса в нашем дочернем классе.
Для разработки примера приложения Hibernate, реализующего ассоциацию «один ко многим», используются следующие технологии и библиотеки:
-
JDK 1.8 или более поздняя версия Hibernate 5 Maven 3 или более поздняя база данных H2
3. Настройка ~~ ~ 3.1. Настройка базы данных
Ниже приведен сценарий нашей базы данных для таблиц Cart и Items. Мы используем ограничение внешнего ключа для сопоставления «один ко многим»:
Наша установка базы данных готова, поэтому давайте перейдем к созданию примера проекта Hibernate.
CREATE TABLE `Cart` (
`cart_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`cart_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
CREATE TABLE `Items` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`cart_id` int(11) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `cart_id` (`cart_id`),
CONSTRAINT `items_ibfk_1` FOREIGN KEY (`cart_id`) REFERENCES `Cart` (`cart_id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
3.2. Зависимости Maven
Затем мы добавим зависимости драйверов Hibernate и H2 в наш файл pom.xml. Зависимость Hibernate использует ведение журнала JBoss и автоматически добавляется в качестве транзитивных зависимостей:
Версия Hibernate 5.2.7. Финальная версия драйвера H2 1.4.197
-
Пожалуйста, посетите центральный репозиторий Maven для получения последних версий Hibernate и H2. зависимости.
3.3. Конфигурация Hibernate
Вот конфигурация Hibernate:
3.4. Класс HibernateAnnotationUtil
<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">org.h2.Driver</property>
<property name="hibernate.connection.password"></property>
<property name="hibernate.connection.url">
jdbc:h2:mem:spring_hibernate_one_to_many</property>
<property name="hibernate.connection.username">sa</property>
<property name="hibernate.dialect">org.hibernate.dialect.H2Dialect</property>
<property name="hibernate.current_session_context_class">thread</property>
<property name="hibernate.show_sql">true</property>
</session-factory>
</hibernate-configuration>
С классом HibernateAnnotationUtil нам просто нужно сослаться на новый файл конфигурации Hibernate:
4. Модели
private static SessionFactory sessionFactory;
private SessionFactory buildSessionFactory() {
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().
configure("hibernate-annotation.cfg.xml").build();
Metadata metadata = new MetadataSources(serviceRegistry).getMetadataBuilder().build();
SessionFactory sessionFactory = metadata.getSessionFactoryBuilder().build();
return sessionFactory;
}
public SessionFactory getSessionFactory() {
if(sessionFactory == null) sessionFactory = buildSessionFactory();
return sessionFactory;
}
Конфигурации, связанные с сопоставлением, будут выполняться с использованием аннотаций JPA в классах моделей: ~ ~~
Обратите внимание, что аннотация @OneToMany используется для определения свойства в классе Items, которое будет использоваться для сопоставления переменной mappedBy. Вот почему у нас есть свойство с именем «cart» в классе Items:
@Entity
@Table(name="CART")
public class Cart {
//...
@OneToMany(mappedBy="cart")
private Set<Items> items;
// getters and setters
}
Также важно отметить, что аннотация @ManyToOne связана с переменной класса Cart. Аннотация @JoinColumn ссылается на сопоставленный столбец.
@Entity
@Table(name="ITEMS")
public class Items {
//...
@ManyToOne
@JoinColumn(name="cart_id", nullable=false)
private Cart cart;
public Items() {}
// getters and setters
}
5. В действии
В тестовой программе мы создаем класс с методом main() для получения сеанса Hibernate и сохраняем объекты модели в базе данных, реализуя ассоциацию «один ко многим»: ~ ~~
Это результат работы нашей тестовой программы:
sessionFactory = HibernateAnnotationUtil.getSessionFactory();
session = sessionFactory.getCurrentSession();
System.out.println("Session created");
tx = session.beginTransaction();
session.save(cart);
session.save(item1);
session.save(item2);
tx.commit();
System.out.println("Cart ID=" + cart.getId());
System.out.println("item1 ID=" + item1.getId()
+ ", Foreign Key Cart ID=" + item.getCart().getId());
System.out.println("item2 ID=" + item2.getId()
+ ", Foreign Key Cart ID=" + item.getCart().getId());
6. Аннотация @ManyToOne
Session created
Hibernate: insert into CART values ()
Hibernate: insert into ITEMS (cart_id)
values (?)
Hibernate: insert into ITEMS (cart_id)
values (?)
Cart ID=7
item1 ID=11, Foreign Key Cart ID=7
item2 ID=12, Foreign Key Cart ID=7
Closing SessionFactory
Как мы видели в разделе 2, мы можем указать отношение «многие к одному», используя Аннотация @ManyToOne. Сопоставление «многие к одному» означает, что многие экземпляры этого объекта сопоставляются с одним экземпляром другого объекта — много товаров в одной корзине.
Аннотация @ManyToOne также позволяет нам создавать двунаправленные отношения. Мы подробно рассмотрим это в следующих нескольких подразделах.
6.1. Несоответствия и право собственности
Теперь, если бы Корзина ссылалась на Товары, а Товары, в свою очередь, не ссылались бы на Корзину, наша связь была бы однонаправленной. Объекты также будут иметь естественную консистенцию.
Однако в нашем случае связь является двунаправленной, что может привести к несогласованности.
«Давайте представим ситуацию, когда разработчик хочет добавить товар1 в корзину, а товар2 в корзину2, но совершает ошибку, из-за которой ссылки между тележкой2 и товаром2 становятся несогласованными:
Как показано выше, элемент2 ссылается на корзину2, тогда как не ссылается на item2, и это плохо.
Cart cart1 = new Cart();
Cart cart2 = new Cart();
Items item1 = new Items(cart1);
Items item2 = new Items(cart2);
Set<Items> itemsSet = new HashSet<Items>();
itemsSet.add(item1);
itemsSet.add(item2);
cart1.setItems(itemsSet); // wrong!
Как Hibernate должен сохранить item2 в базе данных? Будет ли внешний ключ item2 ссылаться на корзину1 или корзину2?
Мы разрешаем эту двусмысленность, используя идею владеющей стороны отношений; ссылки, принадлежащие стороне-владельцу, имеют приоритет и сохраняются в базе данных.
6.2. Элементы как сторона-владелец
Как указано в спецификации JPA в разделе 2.9, рекомендуется помечать сторону многие-к-одному как сторону-владельца.
Другими словами, Items будет стороной-владельцем, а Cart — обратной стороной, что мы и сделали ранее.
Так как же мы этого добились?
Включив атрибут mappedBy в класс Cart, мы помечаем его как обратную сторону.
В то же время мы также аннотируем поле Items.cart с помощью @ManyToOne, делая Items стороной-владельцем.
Возвращаясь к нашему примеру с «несогласованностью», теперь Hibernate знает, что ссылка на item2 более важна, и сохранит ссылку на item2 в базе данных.
Давайте проверим результат:
Несмотря на то, что корзина ссылается на item2 в нашем фрагменте, ссылка item2 на корзину2 сохраняется в базе данных.
item1 ID=1, Foreign Key Cart ID=1
item2 ID=2, Foreign Key Cart ID=2
6.3. Корзина как сторона-владелец
Также можно пометить сторону «один ко многим» как сторону-владельца, а сторону «многие к одному» — как обратную сторону.
Хотя это и не рекомендуется, давайте попробуем.
В приведенном ниже фрагменте кода показана реализация стороны «один ко многим» в качестве стороны-владельца:
Обратите внимание, как мы удалили элемент mappedBy и установили @JoinColumn «многие к одному» как вставляемый и обновляемый до ложный.
public class ItemsOIO {
// ...
@ManyToOne
@JoinColumn(name = "cart_id", insertable = false, updatable = false)
private CartOIO cart;
//..
}
public class CartOIO {
//..
@OneToMany
@JoinColumn(name = "cart_id") // we need to duplicate the physical information
private Set<ItemsOIO> items;
//..
}
Если мы запустим тот же код, результат будет противоположным:
Как показано выше, теперь item2 принадлежит корзине.
item1 ID=1, Foreign Key Cart ID=1
item2 ID=2, Foreign Key Cart ID=1
7. Заключение
Мы увидели, как легко реализовать связь «один ко многим» с Hibernate ORM и базой данных H2, используя аннотации JPA.
Кроме того, мы узнали о двунаправленных отношениях и о том, как реализовать понятие стороны-владельца.
Исходный код этой статьи можно найти на GitHub.
«