Hibernate: Пользовательские типы в hibernate. Разбираемся с компонентами

July 29, 2008

Тема сегодняшней статьи - пользовательские типы в hibernate. Разбираемся с компонентами.

Hibernate служит для отображения java-классов на таблицы БД. Естественно, что бывают ситуации, когда иерархия (сеть) классов java является более "богатой" и не может быть (не должна) переводиться в таблицы БД непосредственно. Классический пример, это класс (таблица) User, который помимо простых свойств fio, age (отображаемых непосредственно на поля таблицы user), содержит более сложные поля. Например, поле homeAddress (тип Address) хранит сведения об домашнем адресе user-а. Можно было бы создать для класса Address собственный mapping и связать классы user & address с помощью ассоциации "один-к-одному", однако это несколько не красиво с точки зрения здравого смысла. Ведь все классы, отображаемые на таблицы, имеют собственный идентификатор и могут быть задействованы в запросах вида:
  1. Session s = SessionFactory.openSession ();
  2. s.get (model.db.Address.class, 12);
  3.  
  4. s.createQuery ('from Address where ...').list ()
Ничего особенно плохого здесь нет, но дело в том, что изначально я планировал, что адрес пользователя не является самостоятельной сущностью и может быть использован только в контексте со своим родительским объектом (user-ом). А раз так, то выделять в самостоятельную entity класс Address не стоит. В hibernate есть два базовых механизм решения приведенной выше проблемы: использование компонентов и разработка собственных типов данных (наиболее гибкое решение).

Компонент - это java класс, который внедряется в состав другого класса. Например, класс AddressComponent представляет собой свойство homeAddress в составе класса User.
  1. package test.db2.model;
  2.  
  3. public class AddressComponent {
  4.     protected String country;
  5.     protected String city;
  6.     protected String home;
  7.     protected String phone;
  8.  
  9.     protected User homeOwner;
  10.  
  11.     public AddressComponent() {
  12.     }
  13.  
  14.     public AddressComponent(String country, String city, String home, String phone) {
  15.         this.country = country;
  16.         this.city = city;
  17.         this.home = home;
  18.         this.phone = phone;
  19.     }
  20.  
  21.     public String getCountry() {
  22.         return country;
  23.     }
  24.  
  25.     public void setCountry(String country) {
  26.         this.country = country;
  27.     }
  28.  
  29.     public String getCity() {
  30.         return city;
  31.     }
  32.  
  33.     public void setCity(String city) {
  34.         this.city = city;
  35.     }
  36.  
  37.     public String getHome() {
  38.         return home;
  39.     }
  40.  
  41.     public void setHome(String home) {
  42.         this.home = home;
  43.     }
  44.  
  45.     public String getPhone() {
  46.         return phone;
  47.     }
  48.  
  49.     public void setPhone(String phone) {
  50.         this.phone = phone;
  51.     }
  52.  
  53.     public User getHomeOwner() {
  54.         return homeOwner;
  55.     }
  56.  
  57.     public void setHomeOwner(User homeOwner) {
  58.         this.homeOwner = homeOwner;
  59.     }
  60. }
Теперь я привожу код класса User:
  1. package test.db2.model;
  2.  
  3. public class User {
  4.     protected Integer id;
  5.     protected String fio;
  6.  
  7.     protected AddressComponent homeAddress;
  8.  
  9.     public User() {}
  10.  
  11.     public User(String fio, AddressComponent homeAddress) {
  12.         this.fio = fio;
  13.         this.homeAddress = homeAddress;
  14.         if (homeAddress != null)
  15.            homeAddress.setHomeOwner(this);
  16.         // *** внимание, это нужно для корректной работы с transient-объектами 
  17.     }
  18.  
  19.     public Integer getId() {
  20.         return id;
  21.     }
  22.  
  23.     public void setId(Integer id) {
  24.         this.id = id;
  25.     }
  26.  
  27.     public String getFio() {
  28.         return fio;
  29.     }
  30.  
  31.     public void setFio(String fio) {
  32.         this.fio = fio;
  33.     }
  34.  
  35.     public AddressComponent getHomeAddress() {
  36.         return homeAddress;
  37.     }
  38.  
  39.     public void setHomeAddress(AddressComponent homeAddress) {
  40.         this.homeAddress = homeAddress;
  41.         if (homeAddress != null)
  42.                 homeAddress.setHomeOwner(this);
  43.         // *** внимание, это нужно для корректной работы с transient-объектами 
  44.     }
  45. }
При создании файлов маппинга, я должен написать правила только для entity-класса, т.е. для User:
  1. <?xml version="1.0"?>
  2. <!DOCTYPE hibernate-mapping PUBLIC
  3.   "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
  4.   "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
  5.  
  6. <hibernate-mapping package="test.db2.model">
  7.  
  8.     <class name="User">
  9.         <id name="id" type="int">
  10.             <generator class="native" />
  11.         </id>
  12.         <property name="fio" type="string" />
  13.         <component name="homeAddress" class="AddressComponent" >
  14.             <parent name="homeOwner" />
  15.             <property name="country" />
  16.             <property name="city" />
  17.             <property name="home" />
  18.             <property name="phone" />
  19.         </component>
  20.     </class>
  21.  
  22. </hibernate-mapping>
Теперь краткий анализ кода: я создал маппинг для класса User, в состав которого входят поля: id, fio. Что касается же поля homeAddress, то это не "примитивное" поле, а объект отображаемый на класс AddressComponent (в том же пакете, что указан как атрибут ). Первый тег "" говорит о том, что в состав класса компонента необходимо внедрить ссылку на родительский объект (посмотрите на описание класса AddressComponent и входящее в его состав поле homOwner). Таким образом, когда я загружаю из сессии объект User, то будет создан и загружен объект AddressComponent, имеющий ссылку на родительский объект. Что касается кода метода setHomeAddress в составе класса User, то для удобства работы с цепочкой владелец-компонент-владелец с transient (т.е. еще не сохраненными в БД объектами), то я явно присваиваю ссылку на владельца адреса в методе setHomeAddress (точно такое же действие срабатывает и в конструкторе класса User):
  1. public void setHomeAddress(AddressComponent homeAddress) {
  2.         this.homeAddress = homeAddress;
  3.         if (homeAddress != null)
  4.                 homeAddress.setHomeOwner(this); // ***
  5.     }
Внутри тега component я должен явно описать какие поля в составе компонента должны быть внедрены в таблицу User. Да, да, в состав таблицы user будут добавлены поля country, city, home, phone:
  1. mysql> SHOW CREATE TABLE user;
  2.  CREATE TABLE `user` (
  3.   `id` int(11) NOT NULL AUTO_INCREMENT,
  4.   `fio` varchar(255) DEFAULT NULL,
  5.   `country` varchar(255) DEFAULT NULL,
  6.   `city` varchar(255) DEFAULT NULL,
  7.   `home` varchar(255) DEFAULT NULL,
  8.   `phone` varchar(255) DEFAULT NULL,
  9.   PRIMARY KEY (`id`)
  10. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
Возникает логичный вопрос: как быть если в состав класса User нужно внедрить два компонента, например, домашний адрес и рабочий. В этом случае необходимо записать все теги "property" с явным указанием того на какое имя поля оно отображается. Например, я добавил в состав класса User поле:
  1. protected AddressComponent workAddress;
А файл маппинга меняю соответствующим образом:
  1. <hibernate-mapping package="test.db2.model">
  2.  
  3.     <class name="User">
  4.         <id name="id" type="int">
  5.             <generator class="native" />
  6.         </id>
  7.         <property name="fio" type="string" />
  8.         <component name="homeAddress" class="AddressComponent" >
  9.             <parent name="homeOwner" />
  10.             <property name="country" column="home_country"  />
  11.             <property name="city" column="home_city" />
  12.             <property name="home" column="home_home" />
  13.             <property name="phone" column="home_phone" />
  14.         </component>
  15.  
  16.         <component name="workAddress" class="AddressComponent" >
  17.             <parent name="homeOwner" />
  18.             <property name="country" column="work_country"  />
  19.             <property name="city" column="work_city" />
  20.             <property name="home" column="work_home" />
  21.             <property name="phone" column="work_phone" />
  22.         </component>
  23.  
  24.     </class>
  25. </hibernate-mapping>
Добавление второго поля workAddress подняло интересный вопрос: А что если у человека нет рабочего адреса? Попробуем например, создать запись User у которой поле workAddress равно null, а затем прочитать эту запись из session и посмотрим: чему будет равно поле workAddress.
  1. public static void main(String[] args) {
  2.         Session session = getSession();
  3.  
  4.         session.beginTransaction();
  5.  
  6.  
  7.         User vasyano = new User("Vasyano Tapkin", new AddressComponent("belarus", "minsk", "13", "1234567890"), null);
  8.         User petyano = new User("Petyano Gromov", new AddressComponent("belarus", "vitebsk", "14", "0987654321"), null);
  9.         User lenka = new User("Lenka Umkina", new AddressComponent("belarus", "grodno", "15", "1029384756"), null);
  10.         lenka.setWorkAddress(null);
  11.         petyano.setWorkAddress(new AddressComponent(null, null, null, null));
  12.         vasyano.setWorkAddress(new AddressComponent(null, null, "13", null));
  13.  
  14.         session.saveOrUpdate(vasyano);
  15.         session.saveOrUpdate(petyano);
  16.         session.saveOrUpdate(lenka);
  17.  
  18.         session.getTransaction().commit();
  19.  
  20.  
  21.         session = getSession();
  22.  
  23.         session.beginTransaction();
  24.  
  25.         User lenka2 = (User) session.load(User.class, lenka.getId());
  26.         User petyano2 = (User) session.load(User.class, petyano.getId());
  27.         User vasyano2 = (User) session.load(User.class, vasyano.getId());
  28.  
  29.         System.out.println("lenka2 = " + lenka2.getWorkAddress());
  30.         System.out.println("petyano2 = " + petyano2.getWorkAddress());
  31.         System.out.println("vasyano2 = " + vasyano2.getWorkAddress());
  32.  
  33.         session.getTransaction().commit();
  34.     }
Обратите внимание на то, что я создал запись lenka c полем workAddress равным null. Для user-a petyano поле workAddress не равно null, но все поля этой записи равны null. Последний же пользователь petyano имеет одно единственное поле "номер_дома" отличным от null. Затем после того как записи были сохранены, я прочитал их из другой сессии и вывожу на экран, чему же равно значение поля workAddress:
lenka2 = null
petyano2 = null
vasyano2 = test.db2.model.AddressComponent@e45b5e
Как видите, для того, чтобы объект поля-компонента был создан необходимо, чтобы хотя бы одно, любое поле, входящее в состав компонента было отлично от null. В противном случае workAddress будет равен null.

Теперь рассмотрим то, как можно работать с компонентами, используя не файлы xml-маппинга, а, именно, аннотации:

Прежде всего я должен аннотировать класс AddressComponent с помощью аннотации @Embeddable (т.е. способный к работе в роли компонента).
  1. package test.db2.model;
  2.  
  3. import org.hibernate.annotations.Parent;
  4.  
  5. import javax.persistence.Embedded;
  6. import javax.persistence.Embeddable;
  7.  
  8. @Embeddable
  9. public class AddressComponent {
  10.     protected String country;
  11.     protected String city;
  12.     protected String home;
  13.     protected String phone;
  14.  
  15.     @Parent
  16.     protected User homeOwner;
  17.  
  18.  
  19.     public AddressComponent() {
  20.     }
  21.  
  22.     public AddressComponent(String country, String city, String home, String phone) {
  23.         this.country = country;
  24.         this.city = city;
  25.         this.home = home;
  26.         this.phone = phone;
  27.     }
  28.  
  29.     public String getCountry() {
  30.         return country;
  31.     }
  32.  
  33.     public void setCountry(String country) {
  34.         this.country = country;
  35.     }
  36.  
  37.     public String getCity() {
  38.         return city;
  39.     }
  40.  
  41.     public void setCity(String city) {
  42.         this.city = city;
  43.     }
  44.  
  45.     public String getHome() {
  46.         return home;
  47.     }
  48.  
  49.     public void setHome(String home) {
  50.         this.home = home;
  51.     }
  52.  
  53.     public String getPhone() {
  54.         return phone;
  55.     }
  56.  
  57.     public void setPhone(String phone) {
  58.         this.phone = phone;
  59.     }
  60.  
  61.     public User getHomeOwner() {
  62.         return homeOwner;
  63.     }
  64.  
  65.     public void setHomeOwner(User homeOwner) {
  66.         this.homeOwner = homeOwner;
  67.     }
  68. }
Теперь в составе класса User я должен поля homeAddress и workAddress пометить с помощью аннотации @Embedded. Казалось-бы, что может быть проще? На самом деле в нашем примере с двумя компонентами в составе User-а возникает проблема с именованием полей. Нам нужно переопределить как будут называться поля для каждого (или одного любого) из компонентов. Для этого используется аннотация @AttributeOverride (если перекрывается одно поле) или @AttributeOverrides, когда перекрывается несколько полей.
  1. @Entity
  2. public class User {
  3.     @Id
  4.     @GeneratedValue
  5.     protected Integer id;
  6.     protected String fio;
  7.  
  8.     @Embedded
  9.     protected AddressComponent homeAddress;
  10.  
  11.     @Embedded
  12.     @AttributeOverrides({
  13.         @AttributeOverride(name = "country", column = @Column(name = "work_country")),
  14.         @AttributeOverride(name = "city", column = @Column(name = "work_city")),
  15.         @AttributeOverride(name = "home", column = @Column(name = "work_home")),
  16.         @AttributeOverride(name = "phone", column = @Column(name = "work_phone"))
  17.     })
  18.     protected AddressComponent workAddress;
  19.  
  20.  ... все как раньше ...
Теперь рассмотрим вопрос: что может быть в составе Component-а (Embeddable)? Могут ли быть там только "простые" поля, или возможно внедрить в состав коллекции сложные типы данных: списки, множества, еще один вложенный component? Ответ: да, можно и очень легко. Прежде всего я создал новый компонент Country в составе двух полей (название страны и ее гимн). Затем я переработал класс AddressComponent, заменив в его составе поле country с типа String на введенный шагом ранее тип Country.
  1. package test.db2.model;
  2.  
  3. import javax.persistence.Embeddable;
  4.  
  5.  
  6. @Embeddable
  7. public class Country {
  8.     protected String name;
  9.     protected String anthem;
  10.  
  11.     public Country() {
  12.     }
  13.  
  14.     public Country(String name, String anthem) {
  15.         this.name = name;
  16.         this.anthem = anthem;
  17.     }
  18.  
  19.     public String getName() {
  20.         return name;
  21.     }
  22.  
  23.     public void setName(String name) {
  24.         this.name = name;
  25.     }
  26.  
  27.     public String getAnthem() {
  28.         return anthem;
  29.     }
  30.  
  31.     public void setAnthem(String anthem) {
  32.         this.anthem = anthem;
  33.     }
  34. }
Для демонстрации методик работы с коллекциями я создал еще один класс-сущность Furniture, который будет представлять сведения об мебели расставленной по указанному адресу.
  1. package test.db2.model;
  2.  
  3. import javax.persistence.Entity;
  4. import javax.persistence.Id;
  5. import javax.persistence.GeneratedValue;
  6. import javax.persistence.GenerationType;
  7.  
  8. @Entity
  9. public class Furniture {
  10.     @Id
  11.     @GeneratedValue(strategy = GenerationType.AUTO)
  12.     Integer id;
  13.  
  14.  
  15.     protected String name;
  16.  
  17.     public Furniture() {
  18.     }
  19.  
  20.     public Furniture(String name) {
  21.         this.name = name;
  22.     }
  23.  
  24.     public Integer getId() {
  25.         return id;
  26.     }
  27.  
  28.     public void setId(Integer id) {
  29.         this.id = id;
  30.     }
  31.  
  32.     public String getName() {
  33.         return name;
  34.     }
  35.  
  36.     public void setName(String name) {
  37.         this.name = name;
  38.     }
  39. }
И теперь код изменного класса AddressComponent (здесь я промаркировал поле country как @Embedded и создал коллекцию furniture с аннотацией @OneToMany):
  1. package test.db2.model;
  2.  
  3. import org.hibernate.annotations.Parent;
  4.  
  5. import javax.persistence.*;
  6. import java.util.List;
  7. import java.util.ArrayList;
  8.  
  9. @Embeddable
  10. public class AddressComponent {
  11.  
  12.     @Embedded
  13.     protected Country country;
  14.     protected String city;
  15.     protected String home;
  16.     protected String phone;
  17.  
  18.     @Parent
  19.     protected User homeOwner;
  20.  
  21.  
  22.     @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
  23.     protected List<Furniture> furniture = new ArrayList<Furniture>();
  24.  
  25.  
  26.     public AddressComponent() {
  27.     }
  28.  
  29.     public AddressComponent(Country country, String city, String home, String phone) {
  30.         this.country = country;
  31.         this.city = city;
  32.         this.home = home;
  33.         this.phone = phone;
  34.     }
  35.  
  36.     public Country getCountry() {
  37.         return country;
  38.     }
  39.  
  40.     public void setCountry(Country country) {
  41.         this.country = country;
  42.     }
  43.  
  44.     public String getCity() {
  45.         return city;
  46.     }
  47.  
  48.     public void setCity(String city) {
  49.         this.city = city;
  50.     }
  51.  
  52.     public String getHome() {
  53.         return home;
  54.     }
  55.  
  56.     public void setHome(String home) {
  57.         this.home = home;
  58.     }
  59.  
  60.     public String getPhone() {
  61.         return phone;
  62.     }
  63.  
  64.     public void setPhone(String phone) {
  65.         this.phone = phone;
  66.     }
  67.  
  68.     public User getHomeOwner() {
  69.         return homeOwner;
  70.     }
  71.  
  72.     public void setHomeOwner(User homeOwner) {
  73.         this.homeOwner = homeOwner;
  74.     }
  75.  
  76.     public List<Furniture> getFurniture() {
  77.         return furniture;
  78.     }
  79.  
  80.     public void setFurniture(List<Furniture> furniture) {
  81.         this.furniture = furniture;
  82.     }
  83. }
В составе класса User также пришлось внести изменения (а казалось то, что зачем?). Все дело в том, что как только я ввел в состав адреса вложенный компонент страна, состоящий из двух полей, и если оставить все на самотек, то у нас снова возникнут проблемы из-за конфликта имен country.name и country.anthem (обратите внимание на составной синтаксис указания имени поля для которого я переопределяю имена):
  1. @Entity
  2. public class User {
  3.     @Id
  4.     @GeneratedValue(strategy = GenerationType.AUTO)
  5.     protected Integer id;
  6.     protected String fio;
  7.  
  8.     @Embedded
  9.     protected AddressComponent homeAddress;
  10.  
  11.     @Embedded
  12.     @AttributeOverrides({
  13.         @AttributeOverride(name = "country.name", column = @Column(name = "work_country_name")),
  14.         @AttributeOverride(name = "country.anthem", column = @Column(name = "work_country_anthem")),
  15.         @AttributeOverride(name = "city", column = @Column(name = "work_city")),
  16.         @AttributeOverride(name = "home", column = @Column(name = "work_home")),
  17.         @AttributeOverride(name = "phone", column = @Column(name = "work_phone"))
  18.             })
  19.     protected AddressComponent workAddress;
Думаете, на этом все? Как бы не так. Внимательные из-вас запомнили что в состав класса AddressComponent был введено поле с коллекцией мебели (furniture), что будет с ним? А будут с ним проблемы. Думаете, все проблемы только в дублях имен полей и будет достаточно переименовать поле furniture во что-то другое? Как бы не так. Начнем с того, что я покажу как выглядит структура данных, сгенерированная hibernate для хранения данных.
mysql> show tables;
+------------------------+
| Tables_in_demoarticles |
+------------------------+
| furniture              |
| user                   |
| user_furniture         |
+------------------------+
3 rows in set (0.00 sec)
 
mysql> show create table furniture;
+-----------+---------------------------------------------------------------------------------
| Table     | Create Table
+-----------+---------------------------------------------------------------------------------
| furniture | CREATE TABLE `furniture` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+-----------+---------------------------------------------------------------------------------
1 row in set (0.00 sec)
 
mysql> show create table user;
--------------------------------------------------------------------------------------------+
| Table | Create Table                                                                      |
+-------+-------------------------------------------------------------------------------------
| user  | CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `fio` varchar(255) DEFAULT NULL,
  `city` varchar(255) DEFAULT NULL,
  `anthem` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `home` varchar(255) DEFAULT NULL,
  `phone` varchar(255) DEFAULT NULL,
  `work_city` varchar(255) DEFAULT NULL,
  `work_country_anthem` varchar(255) DEFAULT NULL,
  `work_country_name` varchar(255) DEFAULT NULL,
  `work_home` varchar(255) DEFAULT NULL,
  `work_phone` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+-------+-------------------------------------------------------------------------------------
1 row in set (0.00 sec)
 
mysql> show create table user_furniture;
----------------------------------------------------------------------------------------------
| Table          | Create Table
+----------------+----------------------------------------------------------------------------
| user_furniture | CREATE TABLE `user_furniture` (
  `User_id` int(11) NOT NULL,
  `furniture_id` int(11) NOT NULL,
  UNIQUE KEY `furniture_id` (`furniture_id`),
  KEY `FK7688287E861E29FF` (`furniture_id`),
  KEY `FK7688287E931FA535` (`User_id`),
  CONSTRAINT `FK7688287E931FA535` FOREIGN KEY (`User_id`) REFERENCES `user` (`id`),
  CONSTRAINT `FK7688287E861E29FF` FOREIGN KEY (`furniture_id`) REFERENCES `furniture` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+----------------+----------------------------------------------------------------------------
1 row in set (0.00 sec)
Видите, в чем проблема? Для организации связи между сущностью пользователь-и-его-какой-то-адрес и сущностью мебель была создана вспомогательная таблица с ассоциацией между user->furniture. Вот только содержимое таблицы user_furniture наводит на размышления. Как интересно hibernate будет определять к какому из адресов некоторого пользователя будет привязана данная единица мебели? Да никак. Вот пример кода, который не работает:
  1. Session session = getSession();
  2.  
  3.         session.beginTransaction();
  4.  
  5.  
  6.         Country belarus = new Country("belarus", "bla-bla-bla");
  7.         User vasyano = new User("Vasyano Tapkin", new AddressComponent(belarus, "minsk", "13", "1234567890"), null);
  8.         User petyano = new User("Petyano Gromov", new AddressComponent(belarus, "vitebsk", "14", "0987654321"), null);
  9.         User lenka = new User("Lenka Umkina", new AddressComponent(belarus, "grodno", "15", "1029384756"), null);
  10.         lenka.setWorkAddress(new AddressComponent(null, null, null, null));
  11.  
  12.         petyano.setWorkAddress(new AddressComponent(null, null, null, null));
  13.         vasyano.setWorkAddress(new AddressComponent(null, null, "13", null));
  14.  
  15.         Furniture table = new Furniture("table");
  16.         Furniture chair = new Furniture("chair");
  17.         Furniture armchair = new Furniture("armchair");
  18.  
  19.         lenka.getHomeAddress().getFurniture().add(table);
  20.         lenka.getHomeAddress().getFurniture().add(chair);
  21.         lenka.getHomeAddress().getFurniture().add(armchair);
  22.  
  23.         //lenka.getWorkAddress().getFurniture().add(table); -- это не будет работать, догадайтесь сами почему?
  24.         lenka.getWorkAddress().getFurniture().add(new Furniture("table"));
  25.  
  26.         session.saveOrUpdate(vasyano);
  27.         session.saveOrUpdate(petyano);
  28.         session.saveOrUpdate(lenka);
  29.  
  30.         session.getTransaction().commit();
  31.  
  32.  
  33.         session = getSession();
  34.  
  35.         session.beginTransaction();
  36.  
  37.         User lenka2 = (User) session.get(User.class, lenka.getId());
  38.         User petyano2 = (User) session.get(User.class, petyano.getId());
  39.         User vasyano2 = (User) session.get(User.class, vasyano.getId());
  40.  
  41.         System.out.println("lenka2 = " + lenka2.getWorkAddress());
  42.         System.out.println("petyano2 = " + petyano2.getWorkAddress());
  43.         System.out.println("vasyano2 = " + vasyano2.getWorkAddress());
  44.  
  45.         session.getTransaction().commit();
Как видите, я пользователю Ленке назначил два адреса: домашний и рабочий. Затем по домашнему адресу были помещены три единицы мебели (стол, стул, кресло). Что касается адреса рабочего, то когда я попытался добавить туда стол, то получил ошибку, так что мне пришлось явно записать new и сдублировать мебель:
  1. //lenka.getWorkAddress().getFurniture().add(table); -- это не будет работать, догадайтесь сами почему?
  2.         lenka.getWorkAddress().getFurniture().add(new Furniture("table"));
Когда я запустил код и вывел на экран содержимое записи о Ленке, то увидел, что по каждому из адресов четыре единицы мебели (одной и той же):



Как вывод: сам hibernate не может разобраться с тем как настроить связи для пары компонент-список элементов, и мы должны ему подсказать об этом. Формально для решения этой проблемы предназначена аннотация @AssociationOverride. Однако я так и не смог заставить ее работать, так что если кто-то знает и умеет больше, пожалуйста, напишите мне - буду благодарен.

Завершая рассказ об Embedded или компонентах, я расскажу об такой редкой (а может совсем и не редкой, если вы работаете с унаследованными БД) ситуации, когда компонент служит для представления не обычного поля в таблице, а играет роль первичного ключа этой таблицы. Для этого я в состав класса User ввожу вложенный public static класс UserID. Натуральный композитный первичный ключ теперь будет образован двумя полями: серия паспорта и номер паспорта. Из обязательных требований к композитному id является то, что нужно декларировать сам класс как @Embeddable, реализовать интерфейс Serializable, и (очень, очень, очень важно) перекрыть методы equals и hashCode для класса.
  1. @Entity
  2. public class User {
  3.  
  4.  
  5.     @Embeddable
  6.     @AccessType(value = "field")
  7.     public static class UserID implements Serializable {
  8.         protected String passport_num;
  9.         protected String passport_serie;
  10.  
  11.         public UserID() {}
  12.  
  13.         public UserID(String passport_num, String passport_serie) {
  14.             this.passport_num = passport_num;
  15.             this.passport_serie = passport_serie;
  16.         }
  17.  
  18.         public int hashCode() {
  19.             return  passport_num.hashCode()*29+passport_serie.hashCode();
  20.         }
  21.  
  22.         public boolean equals(Object obj) {
  23.             if (obj == null) return false;
  24.             if (!(obj instanceof UserID)) return false;
  25.             UserID id = (UserID) obj;
  26.             return (passport_num == null ? id.passport_num == null : passport_num.equals(id.passport_num))
  27.                     && (passport_serie == null ? id.passport_serie == null : passport_serie.equals(id.passport_serie));
  28.  
  29.         }
  30.     }
  31.  
  32.     @EmbeddedId
  33.     protected UserID id;
Как видите при декларации поля id я заменил не только ему тип с Integer на UserID, но и используемую аннотацию с @Id на @EmbeddedID. Теперь я должен выполнить небольшие правки в коде использующем созданные классы, ведь мне теперь нужно назначать значения первичного ключа для таблицы явно и самому (нет больше auto_increment):
  1. Session session = getSession();
  2.  
  3.         session.beginTransaction();
  4.  
  5.  
  6.         Country belarus = new Country("belarus", "bla-bla-bla");
  7.         User vasyano = new User("Vasyano Tapkin", new AddressComponent(belarus, "minsk", "13", "1234567890"), null);
  8.         User petyano = new User("Petyano Gromov", new AddressComponent(belarus, "vitebsk", "14", "0987654321"), null);
  9.         User lenka = new User("Lenka Umkina", new AddressComponent(belarus, "grodno", "15", "1029384756"), null);
  10.  
  11.         // теперь нужно явно задать идентификаторы для этих записей
  12.         vasyano.setId(new User.UserID("123", "code_a" ));
  13.         petyano.setId(new User.UserID("456", "code_a" ));
  14.         lenka.setId(new User.UserID("123", "code_b" ));
Чтобы завершить вопрос об композитных первичных ключах надо рассмотреть ситуацию, когда такие ключи учавствуют в связях таблиц между собой. Для этого я создал еще один класс Department т.е. список отделов в которых работают пользователи. Теперь уникальным идентификатором пользователя будет не только номер и серия паспорта, но и номер отдела, в котором будет работать.
  1. @Entity
  2. public class Department {
  3.  
  4.     @Id
  5.     @GeneratedValue(strategy = GenerationType.AUTO)
  6.     protected Integer id;
  7.  
  8.     protected String name;
  9.  
  10.  
  11.     @OneToMany(mappedBy = "id.department", cascade = CascadeType.ALL)
  12.     @JoinColumn (name = "fk_department_id")        
  13.     Set<User> users = new HashSet<User>();
  14.  
  15.  
  16.     public Department() {}
  17.  
  18.     public Department(String name) {
  19.         this.name = name;
  20.     }
  21.  
  22.     public Integer getId() {
  23.         return id;
  24.     }
  25.  
  26.     public void setId(Integer id) {
  27.         this.id = id;
  28.     }
  29.  
  30.     public String getName() {
  31.         return name;
  32.     }
  33.  
  34.     public void setName(String name) {
  35.         this.name = name;
  36.     }
  37.  
  38.     public Set<User> getUsers() {
  39.         return users;
  40.     }
  41.  
  42.     public void setUsers(Set<User> users) {
  43.         this.users = users;
  44.     }
  45. }
Теперь я вношу правки в состав класса UserID:
  1. @Embeddable
  2.     @AccessType(value = "field")
  3.     public static class UserID implements Serializable {
  4.         public String passport_num;
  5.         public String passport_serie;
  6.  
  7.         @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
  8.         @JoinColumn(name = "fk_department_id")
  9.         public Department department;
  10.  
  11.         public UserID() {
  12.         }
  13.  
  14.         public UserID(String passport_num, String passport_serie, Department department) {
  15.             this.passport_num = passport_num;
  16.             this.passport_serie = passport_serie;
  17.             this.department = department;
  18.         }
  19.  
  20.         public int hashCode() {
  21.             return passport_num.hashCode() * 29 + passport_serie.hashCode() * 17 + department.hashCode();
  22.         }
  23.  
  24.         public boolean equals(Object obj) {
  25.             if (obj == null) return false;
  26.             if (!(obj instanceof UserID)) return false;
  27.             UserID id = (UserID) obj;
  28.             return
  29.                     (passport_num == null ? id.passport_num == null : passport_num.equals(id.passport_num)) &&
  30.                     (passport_serie == null ? id.passport_serie == null : passport_serie.equals(id.passport_serie)) &&
  31.                     (department == null ? id.department == null : department.equals(id.department));
  32.  
  33.         }
  34.     }
Если однако, запустить данный код, то получим не совсем то, что ожидали.
  1. Session session = getSession();
  2.  
  3.         session.beginTransaction();
  4.         Department managers = new Department("managers");
  5.         Department designers = new Department("designers");
  6.  
  7.         Country belarus = new Country("belarus", "bla-bla-bla");
  8.         User vasyano = new User("Vasyano Tapkin", new AddressComponent(belarus, "minsk", "13", "1234567890"), null);
  9.         User petyano = new User("Petyano Gromov", new AddressComponent(belarus, "vitebsk", "14", "0987654321"), null);
  10.         User lenka = new User("Lenka Umkina", new AddressComponent(belarus, "grodno", "15", "1029384756"), null);
  11.  
  12.  
  13.         session.saveOrUpdate(managers);
  14.         session.saveOrUpdate(designers);
  15.  
  16.         // теперь нужно явно задать идентификаторы для этих записей
  17.         vasyano.setId(new User.UserID("123", "code_a", managers));
  18.         petyano.setId(new User.UserID("456", "code_a", managers));
  19.         lenka.setId(new User.UserID("123", "code_b", designers));
  20.  
  21.  
  22.         session.saveOrUpdate(vasyano);
  23.         session.saveOrUpdate(petyano);
  24.         session.saveOrUpdate(lenka);
  25.  
  26.         session.getTransaction().commit();
  27.  
  28.         session = getSession();
  29.  
  30.         session.beginTransaction();
  31.  
  32.         User lenka2 = (User) session.get(User.class, lenka.getId());
  33.  
  34.         Department designers_2 = (Department )session.get(Department.class, 2);
  35.  
  36.         session.getTransaction().commit();
Я привожу пример экрана variables, обратите там внимание на то что переменная designers_2 (ссылка на отдел, в составе которого числится lenka), а затем внимание на значение lenka_2.id.department. Вы видите, что это два разных объекта (у них разные hash_id). Более того, список сотрудников в рамках отдела пуст.



Решением этой проблемы может использование другой стратегии работы с композитным первичным ключом. В этом случае я меняю декларацию класса User на следующую:
  1. @Entity
  2. @IdClass (User.UserID.class)
  3. public class User implements Serializable{
  4.  
  5.  
  6.     @AccessType(value = "field")
  7.     public static class UserID implements Serializable {
  8.  
  9.         public String passport_num;
  10.  
  11.         public String passport_serie;
  12.  
  13.  
  14.         @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
  15.         @JoinColumn(name = "fk_department_id")
  16.         public Department department;
  17.  
  18.         public UserID() {
  19.         }
  20.  
  21.         public UserID(String passport_num, String passport_serie, Department department) {
  22.             this.passport_num = passport_num;
  23.             this.passport_serie = passport_serie;
  24.             this.department = department;
  25.         }
  26.  
  27.         public int hashCode() {
  28.             return passport_num.hashCode() * 29 + passport_serie.hashCode() * 17 + department.hashCode();
  29.         }
  30.  
  31.         public boolean equals(Object obj) {
  32.             if (obj == null) return false;
  33.             if (!(obj instanceof UserID)) return false;
  34.             UserID id = (UserID) obj;
  35.             return
  36.                     (passport_num == null ? id.passport_num == null : passport_num.equals(id.passport_num)) &&
  37.                     (passport_serie == null ? id.passport_serie == null : passport_serie.equals(id.passport_serie)) &&
  38.                     (department == null ? id.department == null : department.equals(id.department));
  39.  
  40.         }
  41.     }
  42.  
  43.     @Id
  44.     public String passport_num;
  45.     @Id
  46.     public String passport_serie;
  47.  
  48.     @Id
  49.     @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
  50.     @JoinColumn(name = "fk_department_id")
  51.     public Department department;
Как видите я убрал из объявления класса UserID аннотацию @Embeddable. Также я для класса User добавил аннотацию @IdClass (User.UserID.class), указав имя класса, который будет играть роль первичного ключа. Затем я продублировал в состав класса User все поля образующие первичный ключ (скопировал описания полей passport_num passport_serie, department из класса UserID). Также каждое из этих дублированных свойств я промаркировал аннотацией @Id.

Небольшие правки были внесены и в состав класса Department:
  1. @Entity
  2. public class Department {
  3.  
  4.     @Id
  5.     @GeneratedValue(strategy = GenerationType.AUTO)
  6.     protected Integer id;
  7.  
  8.     protected String name;
  9.  
  10.  
  11.     @OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
  12.     @JoinColumn (name = "fk_department_id")        
  13.     Set<User> users = new HashSet<User>();
  14.  
  15.  
  16.     public Department() {}
  17.  
  18.     public Department(String name) {
  19.         this.name = name;
  20.     }
  21.  
  22.     public Integer getId() {
  23.         return id;
  24.     }
  25.  
  26.     public void setId(Integer id) {
  27.         this.id = id;
  28.     }
  29.  
  30.     public String getName() {
  31.         return name;
  32.     }
  33.  
  34.     public void setName(String name) {
  35.         this.name = name;
  36.     }
  37.  
  38.     public Set<User> getUsers() {
  39.         return users;
  40.     }
  41.  
  42.     public void setUsers(Set<User> users) {
  43.         this.users = users;
  44.     }
  45. }
Теперь я переписал код работающий с БД:
  1. Session session = getSession();
  2.  
  3. session.beginTransaction();
  4.  
  5. Department managers = new Department("managers");
  6. Department designers = new Department("designers");
  7. Country belarus = new Country("belarus", "bla-bla-bla");
  8. User vasyano = new User("Vasyano Tapkin", new AddressComponent(belarus, "minsk", "13", "1234567890"), null);
  9. User petyano = new User("Petyano Gromov", new AddressComponent(belarus, "vitebsk", "14", "0987654321"), null);
  10. User lenka = new User("Lenka Umkina", new AddressComponent(belarus, "grodno", "15", "1029384756"), null);
  11.  
  12. session.saveOrUpdate(managers);
  13. session.saveOrUpdate(designers);
  14.  
  15. // теперь нужно явно задать идентификаторы для этих записей
  16. vasyano.setPassport_serie("code_a");
  17. vasyano.setPassport_num("123");
  18. vasyano.setDepartment(managers);
  19.  
  20. petyano.setPassport_serie("code_b");
  21. petyano.setPassport_num("455");
  22. petyano.setDepartment(managers);
  23.  
  24. lenka.setPassport_serie("code_a");
  25. lenka.setPassport_num("321");
  26. lenka.setDepartment(designers);
  27.  
  28. session.saveOrUpdate(vasyano);
  29. session.saveOrUpdate(petyano);
  30. session.saveOrUpdate(lenka);
  31.  
  32. session.getTransaction().commit();
  33.  
  34. session = getSession();
  35.  
  36. session.beginTransaction();
  37.  
  38. // нужно обязательно загрузить запись об отделе в рамках текущей сессии 
  39. Department designers_2 = (Department )session.get(Department.class, 2);
  40. //а теперь можно загрузить сведения и об человеке
  41. User lenka2 = (User) session.get(User.class, new User.UserID("321", "code_a", designers_2) );
  42.  
  43. session.getTransaction().commit();
Если запустить данный фрагмент кода, то у загруженной записи "lenka" будет в качестве поля department ссылка на полноценный отдел с заполненными сведениями об сотрудниках.

Categories: Java