Hibernate: отображая иерархии классов

June 26, 2008

Тема сегодняшней статьи – как отобразить иерархию классов на реляционную модель данных. Наследование – это один из столпов ООП, а раз в СУБД нет родного понятия или методики представления подобного отношения, то все что нам остается – это имитировать наследование классов различными способами. В hibernate есть три методики имитации: “вся иерархия классов в одной таблице”, “одна таблица базовому классу и каждому подклассу по таблице дополнений”, “каждому классу свою, независимую от остальных таблицу”. Продолжая наш пример с сотрудниками и отделами, представим, что у каждого сотрудника есть свое любимое животное (возможно, не одно). И создадим иерархию: Животное -> Кошка, Собака -> Тигр и т.д.



Корень иерархии – класс “Animal”, вот его код и пример его маппинга:
  1. /**
  2.  * Базовый класс для иерархии любимых зверюшек сотрудника.
  3.  */
  4. abstract public class Animal {
  5.     // идентификатор нашего животного
  6.     Integer animal_id;
  7.     // имя
  8.     String name;
  9.     // дата рождения
  10.     Calendar birthday;
  11.  
  12.     public Animal() {
  13.     }
  14.  
  15.     public Animal(String name, Calendar birthday) {
  16.         this.name = name;
  17.         this.birthday = birthday;
  18.     }
  19.  
  20.     public Integer getAnimal_id() {
  21.         return animal_id;
  22.     }
  23.  
  24.     public void setAnimal_id(Integer animal_id) {
  25.         this.animal_id = animal_id;
  26.     }
  27.  
  28.     public String getName() {
  29.         return name;
  30.     }
  31.  
  32.     public void setName(String name) {
  33.         this.name = name;
  34.     }
  35.  
  36.     public Calendar getBirthday() {
  37.         return birthday;
  38.     }
  39.  
  40.     public void setBirthday(Calendar birthday) {
  41.         this.birthday = birthday;
  42.     }
  43. }
Ничего сложного здесь нет, разве что я решил, что этот класс будет абстрактным т.к. “просто” животным владеть нельзя. Теперь нужно начать описывать классы наследники: кошку, собаку и тигра. Сначала код для кошки:
  1. /**
  2.  * Кошка
  3.  */
  4. public class Cat extends Animal {
  5.     // цвет глаз кошки
  6.     String eyesColor;
  7.  
  8.     public Cat() {
  9.     }
  10.  
  11.     public Cat(String name, Calendar birthday, String eyesColor) {
  12.         super(name, birthday);
  13.         this.eyesColor = eyesColor;
  14.     }
  15.  
  16.     public String getEyesColor() {
  17.         return eyesColor;
  18.     }
  19.  
  20.     public void setEyesColor(String eyesColor) {
  21.         this.eyesColor = eyesColor;
  22.     }
  23. }
Теперь код для класса Собака:
  1. /**
  2.  * Собака
  3.  */
  4. public class Dog extends Animal {
  5.     // вес собаки
  6.     BigDecimal weight;
  7.  
  8.     public Dog() {
  9.     }
  10.  
  11.     public Dog(String name, Calendar birthday, BigDecimal weight) {
  12.         super(name, birthday);
  13.         this.weight = weight;
  14.     }
  15.  
  16.     public BigDecimal getWeight() {
  17.         return weight;
  18.     }
  19.  
  20.     public void setWeight(BigDecimal weight) {
  21.         this.weight = weight;
  22.     }
  23. }
И, последним, идет код класса Тигр:
  1. /**
  2.  * Тигр, мда ... домашнее животное
  3.  */
  4. public class Tiger extends Cat {
  5.     // количество съеденных сотрудников
  6.     Integer countEatenExployees;
  7.  
  8.     public Tiger() {
  9.     }
  10.  
  11.     public Tiger(String name, Calendar birthday, String eyesColor, Integer countEatenExployees) {
  12.         super(name, birthday, eyesColor);
  13.         this.countEatenExployees = countEatenExployees;
  14.     }
  15.  
  16.     public Integer getCountEatenExployees() {
  17.         return countEatenExployees;
  18.     }
  19.  
  20.     public void setCountEatenExployees(Integer countEatenExployees) {
  21.         this.countEatenExployees = countEatenExployees;
  22.     }
  23. }
Теперь нужно разобраться с файлами маппинга для этих классов. Обратите внимание на то, что здесь я должен решить, как именно на уровне СУБД у меня будут представлены эти классы. Я использую стратегию “вся иерархия классов в одной таблице”. В этом случае классы наследники я должен маркировать не тегом “class”, а “subclass”. Более того, я могу выбрать, где именно их описать: можно в одном файле записать всю иерархию, например, так:
  1. <hibernate-mapping package="experimental.business.animals">
  2.     <class name="Animal" abstract="true">
  3.         <id name="animal_id">
  4.             <generator class="native" />
  5.         </id>
  6.         <property name="birthday" type="calendar"/>
  7.         <property name="name" type="string"/>
  8.  
  9.         <subclass name="Dog">
  10.             <!-- ..... бла-бла-бла -->
  11.         </subclass>
  12.     </class>
  13.  
  14.     <subclass name="Cat" extends="Animal">
  15.         <!-- ..... бла-бла-бла -->
  16.         <subclass name="Tiger">
  17.             <!-- ..... бла-бла-бла -->
  18.         </subclass>
  19.     </subclass>
  20. </hibernate-mapping>
Как видите, я либо помещаю внутрь родительского класса маркер “subclass”, либо выношу декларирование подкласса за пределы родительского тега, но в этом случае мне нужно указать значение атрибута extends – имя класса от которого наследуется наш subclass.

Я, однако, предпочитаю другой вариант, когда каждому классу наследнику дается свой отдельный *.hbm.xml файл (особенно, если иерархия большая или велико количество полей в каждом из классов).

Однако в этом примере чего-то не хватает. Давайте задумаемся, раз у на все классы будут храниться в одной таблице, то эта таблица должна быть общим множеством всех полей и в базовом классе и во всех классах наследниках – просто для некоторых записей значение набора полей будет пустым. Однако, остается открытым вопрос как hibernate на стадии чтения данных из СУБД сможет узнать что вот эта, конкретная, запись должна быть отображена в виде класса Cat или Dog. Существующих полей для принятия решения совершенно недостаточно: мало ли по какой причине поля будут равны null. Лучшим выходом из сложившейся ситуации будет создание специального маркерного поля. Его называют дискриминатором) в котором будет храниться имя того класса который нужно создать. Вообще, хранить можно, что угодно, даже не полные имена классов, а цифры или короткие коды (C,D,T), только это неудобочитаемо, а экономить на спичках (используя короткие коды) – глупо. Итак, теперь пример обновленных файлов маппинга уже с учетом дискриминатора (точно такое же решение используется и в других ORM frameworks, например, ibatis):
  1. <class name="Animal" abstract="true" discriminator-value="Animal">
  2.     <id name="animal_id">
  3.         <generator class="native" />
  4.      </id>
  5.     <discriminator column="DSK" type="string" length="30" not-null="true"/>
  6.     <property name="birthday" type="calendar"/>
  7.     <property name="name" type="string"/>
  8. </class>
  9.  
  10.  
  11. <hibernate-mapping package="experimental.business.animals">
  12.     <subclass name="Cat" extends="Animal" discriminator-value="Cat">
  13.         <property name="eyesColor" type="string"/>
  14.     </subclass>
  15. </hibernate-mapping>
  16.  
  17. <hibernate-mapping package="experimental.business.animals">
  18.     <subclass name="Dog" extends="Animal" discriminator-value="Dog">
  19.         <property name="weight" type="big_decimal"/>
  20.     </subclass>
  21. </hibernate-mapping>
  22.  
  23. <hibernate-mapping package="experimental.business.animals">
  24.     <subclass name="Tiger" extends="Cat" discriminator-value="Tiger">
  25.         <property name="countEatenExployees" type="int"/>
  26.     </subclass>
  27. </hibernate-mapping>
В корневом классе Animal я добавил вложенный элемент discriminator, указав имя колонки которая должна быть создана в базе данных, ее тип (строка) и размер. Теперь нужно в каждом из классов указать значение дискриминатора. Эта строка задается в заголовке класса (я сделал это и для Animal и для Cat, Dog, Tiger). Давайте сгенерируем структуру базы данных и посмотрим, как были представлены наши классы на низком уровне:
  1. CREATE TABLE `animal` (
  2.   `animal_id` int(11) NOT NULL AUTO_INCREMENT,
  3.   `DSK` varchar(30) NOT NULL,
  4.   `birthday` datetime DEFAULT NULL,
  5.   `name` varchar(255) DEFAULT NULL,
  6.   `eyesColor` varchar(255) DEFAULT NULL,
  7.   `weight` decimal(19,2) DEFAULT NULL,
  8.   `countEatenExployees` int(11) DEFAULT NULL,
  9.   PRIMARY KEY (`animal_id`)
  10. ) ENGINE=InnoDB
mysql> select * from animal;
+-----------+-------+---------------------+-------+-----------+--------+---------------------+
| animal_id | DSK   | birthday            | name  | eyesColor | weight | countEatenExployees |
+-----------+-------+---------------------+-------+-----------+--------+---------------------+
|         1 | Cat   | 2007-02-01 00:00:00 | Murka | red       |   NULL |                NULL |
|         2 | Dog   | 2001-02-01 00:00:00 | Bobik | NULL      | 120.00 |                NULL |
|         3 | Tiger | 1978-02-01 00:00:00 | Bazz  | green     |   NULL |                  12 |
+-----------+-------+---------------------+-------+-----------+--------+---------------------+
3 rows in set (0.00 sec)
Все как я и обещал: общая таблица со свойствами всех классов в иерархии (часть полей пустая, как и ожидалось) и колонка DSK, хранящая название класса, который должен быть воссоздан из этой записи.

Теперь какие есть подводные камни при работе с этими полиморфными классами?

Давайте посмотрим на следующий фрагмент кода:
  1. Animal ani_1 = (Animal) ses.load(Animal.class, 1);
  2.  
  3. System.out.println("is it Animal ? " + (ani_1 instanceof Animal) );
  4. System.out.println("is it Cat ? " + (ani_1 instanceof Cat) );
  5. System.out.println("is it Dog ? " + (ani_1 instanceof Dog) );
  6. System.out.println("is it Tiger ? " + (ani_1 instanceof Tiger) );
Его результатом будет:
is it Animal ? true
is it Cat ? false
is it Dog ? false
is it Tiger ? false
Как же так, ведь хотя бы одно из утверждений (кошка ли это, собака или, может, тигр должно было сработать?). Нет: когда мы получаем объект из hibernate session, то на самом деле мы получаем заглушку proxy, сгенерированный, для того чтобы реализовать ту же самую lazy-загрузку и т.д. Генерируемый с помощью cglib класс является наследником от Animal, но никоим образом не будет являться конкретным классом-наследником от Cat, Dog, Tiger. С другой стороны, если я при загрузке объекта из session явно укажу его класс, например, так:
  1. Cat murka_2 = (Cat) ses.load(Cat.class , 1);
  2. Dog bobik_2 = (Dog) ses.load(Dog.class , 1);
  3. System.out.println("murka_2 = " + murka_2);
  4. System.out.println("bobik_2 = " + bobik_2);
В первом случае будет загружен и создан именно объект Cat, а во втором (когда я пытаюсь загрузить переменную не того типа данных), то мне будет возвращено значение null или выброшено исключение (в зависимости от того используете вы load или get).

На этом все про способ организации наследования “все классы иерархии в одной таблице”. Второй вид организации наследования предполагает существование одной базовой таблицы (для корня нашей иерархии – класса Animal). Что же касается остальных классов наследников, то для каждого из них будет создана своя отдельная таблица. Но в этой таблице будут содержаться лишь те поля, которые являются специфическими для этого класса. Естественно, что от каждого из таких подчиненных классов к клавному классу/таблице должна быть протянута на уровне СУБД связь “один-ко-многим” (все это делается hibernate-ом прозрачно и ни о чем не нужно беспокоиться, почти …).

Для демонстрации нового вида организации наследования я не будут никак исправлять код java-классов. Все изменения будут только в файлах маппинга:
  1. <class name="Animal" abstract="true" >
  2.    <id name="animal_id">
  3.      <generator class="native" />
  4.    </id>
  5.    <property name="birthday" type="calendar"/>
  6.    <property name="name" type="string"/>
  7. </class>
  8.  
  9. <joined-subclass name="Cat" extends="Animal" >
  10.    <key column="fk_animal_id" not-null="true" />
  11.    <property name="eyesColor" type="string"/>
  12. </joined-subclass>
  13.  
  14. <joined-subclass name="Dog" extends="Animal">
  15.    <key column="fk_animal_id" not-null="true" />
  16.    <property name="weight" type="big_decimal"/>
  17. </joined-subclass>
  18.  
  19. <joined-subclass name="Tiger" extends="Cat">
  20.    <key column="fk_cat_id" not-null="true" />
  21.    <property name="countEatenExployees" type="int"/>
  22. </joined-subclass>
Как видите, изменения минимальны: в файле маппинга для Animal я избавился от элемента discriminator (сейчас проверка того к какому типу данных принадлежит некоторая запись, выполняется поиском в одной из связанных таблиц ассоциированной записи). Из всех деклараций классов я выкинул также discriminator-value, заменил имя тега с subclass на joined-subclass (вполне таки, говорящее название). Главное изменение в другом: к каждому из subclass-ов я добавил еще одно фиктивное поле fk_animal_id (только для Cat и для Dog). Это поле ссылается на первичный ключ в таблице animal (обязательно нужно указать, что значение данного поля не может быть null). Что касается tiger, то раз он производен от cat, то название поля, которое будет связывать его с, уже не animal, а cat, называется fk_cat_id.

Теперь смотрим на изменения, которые произошли в структуре базы данных:
mysql> show create table animal;
 
CREATE TABLE `animal` (
  `animal_id` int(11) NOT NULL AUTO_INCREMENT,
  `birthday` datetime DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`animal_id`)
) ENGINE=InnoDB 
1 row in set (0.00 sec)
 
mysql> select * from animal;
+-----------+---------------------+-------+
| animal_id | birthday            | name  |
+-----------+---------------------+-------+
|         1 | 2007-02-01 00:00:00 | Murka |
|         2 | 2001-02-01 00:00:00 | Bobik |
|         3 | 1978-02-01 00:00:00 | Bazz  |
+-----------+---------------------+-------+
3 rows in set (0.00 sec)
 
mysql> show create table cat;
 
CREATE TABLE `cat` (
  `fk_animal_id` int(11) NOT NULL,
  `eyesColor` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`fk_animal_id`),
  KEY `FK107B63992321F` (`fk_animal_id`),
  CONSTRAINT `FK107B63992321F` FOREIGN KEY (`fk_animal_id`) REFERENCES `animal` (`animal_id`)
) ENGINE=InnoDB 
1 row in set (0.01 sec)
 
mysql> select * from cat;
+--------------+-----------+
| fk_animal_id | eyesColor |
+--------------+-----------+
|            1 | red       |
|            3 | green     |
+--------------+-----------+
2 rows in set (0.02 sec)
 
mysql> show create table dog;
 
CREATE TABLE `dog` (
  `fk_animal_id` int(11) NOT NULL,
  `weight` decimal(19,2) DEFAULT NULL,
  PRIMARY KEY (`fk_animal_id`),
  KEY `FK10D1C3992321F` (`fk_animal_id`),
  CONSTRAINT `FK10D1C3992321F` FOREIGN KEY (`fk_animal_id`) REFERENCES `animal` (`animal_id`)
) ENGINE=InnoDB 
1 row in set (0.00 sec)
 
mysql> select * from dog;
+--------------+--------+
| fk_animal_id | weight |
+--------------+--------+
|            2 | 120.00 |
+--------------+--------+
1 row in set (0.00 sec)
 
mysql> show create table tiger;
 
CREATE TABLE `tiger` (
  `fk_cat_id` int(11) NOT NULL,
  `countEatenExployees` int(11) DEFAULT NULL,
  PRIMARY KEY (`fk_cat_id`),
  KEY `FK4D1009FC383DA35` (`fk_cat_id`),
  CONSTRAINT `FK4D1009FC383DA35` FOREIGN KEY (`fk_cat_id`) REFERENCES `cat` (`fk_animal_id`)
) ENGINE=InnoDB 
1 row in set (0.00 sec)
 
mysql> select * from tiger;
+-----------+---------------------+
| fk_cat_id | countEatenExployees |
+-----------+---------------------+
|         3 |                  12 |
+-----------+---------------------+
1 row in set (0.00 sec)
Обратите внимание на то, что поле fk_animal_id является одновременно и первичным ключом и внешним. А как же иначе? Ведь не может быть для одной записи в главной таблице Animal двух подчиненных в таблице Cat. И вот пример картинки со связями:



Теперь попробуем вернуться к изначальному заданию и созданную иерархию домашних животных (ага, особенно тигру) привяжем к сотруднику. Связь будет “один-ко-многим” и двунаправленная.

А вот пример использования такой связи:
  1. ses.beginTransaction();
  2.  
  3. Calendar birthMurka = new GregorianCalendar(2007, 1, 1);
  4. Cat murka = new Cat("Murka", birthMurka, "red");
  5. Dog bobik = new Dog("Bobik", new GregorianCalendar(2001, 1, 1), new BigDecimal(120.0));
  6. Tiger bazz = new Tiger("Bazz", new GregorianCalendar(1978, 1, 1), "green", 12);
  7.  
  8. Department designers = new Department("designers");
  9. Employee lena = new Employee("lena");
  10. lena.setDepartment(designers);
  11. designers.getEmployies().add(lena);
  12. lena.getAnimals().add(murka);
  13. lena.getAnimals().add(bobik);
  14. lena.getAnimals().add(bazz);
  15. murka.setOwner(lena);
  16. bobik.setOwner(lena);
  17. bazz.setOwner(lena);
  18.  
  19. ses.saveOrUpdate(lena);
  20.  
  21. ses.getTransaction().commit();
  22.  
  23. System.out.println("------------------------------------------");
  24.  
  25. ses = factory.openSession();
  26. ses.beginTransaction();
  27.  
  28. Employee lena_2 = (Employee) ses.load(Employee.class, lena.getEmployee_id());
  29.  
  30. ses.delete(lena_2);
  31.  
  32. ses.getTransaction().commit();
При сохранении благодаря правильно настроенным каскадным операциям, я при сохранении сотрудника, сохранил и всех животных, которыми владеет Лена. Похожие действия были выполнены и на стадии удаления – увольнение сотрудника привело к гибели (наверное, от голода) всех его домашних животных.
  1. <class name="Animal" abstract="true" >
  2.    <id name="animal_id">
  3.       <generator class="native" />
  4.    </id>
  5.    <property name="birthday" type="calendar"/>
  6.    <property name="name" type="string"/>
  7.  
  8.    <many-to-one name="owner" class="experimental.business.Employee" column="fk_employee_id" 
  9.                      cascade="save-update" not-null="true" />
  10. </class>
Маппинг для корневого класса Animal не слишком изменился: я добавил элемент many-to-one (обратите внимание на указание полного пути к классу Employee, т.к. теперь у меня Animal и Employee находятся в разных пакетах). Включенные каскадные операции на сохранение (никого удаления, если мы не хотим, чтобы внезапная смерть любимого попугайчика не привела к такой же неожиданной гибели сотрудника). Изменения на второй строне – файле маппинга для класса Employee синхронны:
  1. <class name="Employee" dynamic-insert="true" dynamic-update="true" >
  2.   <id name="employee_id">
  3.      <generator class="native"/>
  4.   </id>
  5.   <property name="fio"/>
  6.   <many-to-one name="department" class="Department" column="fk_department_id" not-null="true"
  7.             cascade="save-update"/>
  8.   <property name="bio" lazy="true" type="text"/>
  9.   <set name="animals" lazy="false" fetch="join" cascade="all" inverse="true">
  10.      <key column="fk_employee_id" not-null="true" />
  11.      <one-to-many class="experimental.business.animals.Animal"  />
  12.   </set>
  13. </class>
Не забываем пометить сторону “Один” в нашей двунаправленной связи атрибутом inverse. Теперь смотрим, какие изменения произошли в классах Employee и Animal:
  1. public class Employee {
  2.     Integer employee_id;
  3.     String fio;
  4.     Department department;
  5.     Integer uo;
  6.     String bio;
  7.  
  8.     // список домаших животных сотрудника
  9.     Set <Animal> animals = new HashSet<Animal>();
  10.     public Set<Animal> getAnimals() {
  11.         return animals;
  12.     }
  13.     public void setAnimals(Set<Animal> animals) {
  14.         this.animals = animals;
  15.     }
  16.     // ... 
  17. }
  18.  
  19.  
  20. /**
  21.  * Базовый класс для иерархии любимых зверюшек сотрудника.
  22.  */
  23. abstract public class Animal {
  24.     // идентификатор нашего животного
  25.     Integer animal_id;
  26.     // имя
  27.     String name;
  28.     // дата рождения
  29.     Calendar birthday;
  30.  
  31.     Employee owner;
  32.  
  33.     public Employee getOwner() {
  34.         return owner;
  35.     }
  36.  
  37.     public void setOwner(Employee owner) {
  38.         this.owner = owner;
  39.     }
  40.     // ... 
  41. }
Теперь последний и самый плохой (я вообще никогда его не использовал) способ сохранить иерархию классов – “каждому классу по таблице”. Этот способ не похож на “каждому из подклассов по таблице”, т.к. класс Cat будет содержать не одно поле “цвет глаз кошки”, но и все поля которые были получены по наследству. Также никакой связи на уровне СУБД в виде внешнего ключа от таблицы Cat к таблице Animal не будет. Такой способ организации отношений добавляет массу проблем как при написании запросов на отбор данных, так и при хранении коллекции и связей между классами. Упростим наш пример и предположим, что у сотрудника нет списка любимых животных, а только одно. И связь мы организуем в направлении к животному. Т.е. мы не будем добавлять внешний ключ в таблицу animal (номер хозяина), а наоборот, добавим к сотруднику сведения о том, каким животным он владеет. И тут нас ждет засада: как на уровне СУБД представить такую связь? Добавить в таблицу Сотрудника, поле хранящее номер Животного не возможно т.к. … а какой номер вы хотите добавить, ведь у нас теперь для каждого вида зверья своя отдельная, со своим идентификатором, таблица. Когда мы использовали схему “один класс для всего” и “базовый класс и связанные с ним классы-таблицы”, то такой проблемы возникнуть не могло. Единственным решением будет добавить в таблицу Сотрудник два поля, одно из них будет хранить номер Животного, а второе … а второе имя таблицы, в которой нужно искать запись с этим самым номером.

Начнем с того, что полностью перепишем файлы маппинга (сами классы с иерархией животных я трогать не буду):
  1. <class name="Cat">
  2.    <id name="animal_id">
  3.        <generator class="native"/>
  4.    </id>
  5.    <property name="birthday" type="calendar"/>
  6.    <property name="name" type="string"/>
  7.  
  8.    <many-to-one name="owner" class="experimental.business.Employee" column="fk_employee_id"
  9.       cascade="save-update" not-null="true"/>
  10.    <property name="eyesColor" type="string"/>
  11. </class>
  12.  
  13. <class name="Dog">
  14.    <id name="animal_id">
  15.      <generator class="native"/>
  16.    </id>
  17.    <property name="birthday" type="calendar"/>
  18.    <property name="name" type="string"/>
  19.  
  20.    <many-to-one name="owner" class="experimental.business.Employee" column="fk_employee_id"
  21.         cascade="save-update" not-null="true"/>
  22.    <property name="weight" type="big_decimal"/>
  23. </class>
  24.  
  25. <class name="Tiger">
  26.   <id name="animal_id">
  27.       <generator class="native"/>
  28.   </id>
  29.   <property name="birthday" type="calendar"/>
  30.   <property name="name" type="string"/>
  31.  
  32.   <many-to-one name="owner" class="experimental.business.Employee" column="fk_employee_id"
  33.                   cascade="save-update" not-null="true"/>
  34.   <property name="eyesColor" type="string"/>
  35.   <property name="countEatenExployees" type="int"/>
  36. </class>
Что я вижу? А вижу я сплошные дубляжи: я скопировал все содержимое маппинга Animal в маппинг для Cat и Dog, а затем содержимое маппинга Cat перенеслось в маппинг для Tiger. Да … дубляжи – это все наше. Одно радует, что процесс написания маппинга выполняется не так часто.

Смотрим изменения в маппинге для сотрудника:
  1. <class name="Employee" dynamic-insert="true" dynamic-update="true" >
  2.    <id name="employee_id">
  3.        <generator class="native"/>
  4.    </id>
  5.    <property name="fio"/>
  6.    <many-to-one name="department" class="Department" column="fk_department_id" not-null="true"
  7.           cascade="save-update"/>
  8.    <property name="bio" lazy="true" type="text"/>
  9.    <one-to-one name="car" class="Car" cascade="all"/>
  10.  
  11.    <any meta-type="string" id-type="integer" name="friend"  cascade=”save-update”>
  12.        <column name="fk_animal_kind" />
  13.        <column name="fk_animal_id" />
  14.    </any>
  15. </class>
Мы встречаемся с новым элементом any. В качестве параметра мы указываем для него name – имя поля в классе Employee хранящего ссылку на домашнее животное. И, самое важное, нужно указать каким образом определяется то, какой из трех видов животных нужно создать. Для этого мы задаем два поля (порядок тегов column важен). Первое из них хранит вид животного (декларация meta-type="string" говорит, что в этом поле будет храниться строка с названием класса). Второе поле fk_animal_id, будет хранить уже идентификатор для выбранной на предыдущем шаге таблицы.

Можно записать “переключатель типов” и подобным образом (в качестве ключей используются не имена классов, а произвольные строки).
  1. <any meta-type="string" id-type="integer" name="friend"  cascade=”save-update”>
  2.     <meta-value value="C" class="Cat"/> 
  3.     <meta-value value="D" class="Dog"/> 
  4.     <meta-value value="T" class="Tiger"/> 
  5.     <column name="fk_animal_kind" />
  6.     <column name="fk_animal_id" />
  7. </any>
В результате запуска приложения будет сгенерирована следующая структура данных:
  1. CREATE TABLE `employee` (
  2.   `employee_id` int(11) NOT NULL AUTO_INCREMENT,
  3.   `fio` varchar(255) DEFAULT NULL,
  4.   `fk_department_id` int(11) NOT NULL,
  5.   `bio` text,
  6.   `fk_animal_kind` varchar(255) DEFAULT NULL,
  7.   `fk_animal_id` int(11) DEFAULT NULL,
  8.   PRIMARY KEY (`employee_id`),
  9.   KEY `FK4AFD4ACE28F13B88` (`fk_department_id`),
  10.   CONSTRAINT `FK4AFD4ACE28F13B88` FOREIGN KEY (`fk_department_id`) REFERENCES `department` (`department_id`)
  11. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
А вот пример того как будут сохранены данные в таблице:
mysql> select * from employee;
+-------------+------+------------------+------+-------------------------------------+--------------+
| employee_id | fio  | fk_department_id | bio  | fk_animal_kind                      | fk_animal_id |
+-------------+------+------------------+------+-------------------------------------+--------------+
|           1 | lena |                1 | NULL | experimental.business.animals.Tiger |            1 |
+-------------+------+------------------+------+-------------------------------------+--------------+
1 row in set (0.02 sec)
Теперь смотрим какие изменения произошли в классе Employee (добавили поле friend):
  1. public class Employee {
  2.     Integer employee_id;
  3.     String fio;
  4.     Department department;
  5.     Integer uo;
  6.     String bio;
  7.  
  8.     // лучший друг сотрудника (демонстрация связи any)
  9.     Animal friend;
  10.  
  11.     public Animal getFriend() {
  12.         return friend;
  13.     }
  14.  
  15.     public void setFriend(Animal friend) {
  16.         this.friend = friend;
  17.     }
  18.     // ... 
  19. }
Последний шаг – тестирование:
  1. Department designers = new Department("designers");
  2. Employee lena = new Employee("lena");
  3. lena.setDepartment(designers);
  4. designers.getEmployies().add(lena);
  5.  
  6. bazz.setOwner(lena);
  7. lena.setFriend(bazz);
  8.  
  9. ses.saveOrUpdate(bazz);
  10. ses.saveOrUpdate(lena);
  11.  
  12. ses.getTransaction().commit();
Сходным образом можно организовать и коллекцию, элементами которой будут классы в “той самой стратегии”. Однако сделать так можно, только если коллекция будет служить для отображения на иерархию многие-ко-многим (один-ко-многим не поддерживается). На этих словах я специально полез в доку по hibernate и вырезал оттуда фрагментик со сводной таблицей “что можно и чего нельзя делать” (см. конец статьи).

Вот так будет выглядеть обновленный маппинг для сотрудника:
  1. <set name="animals" lazy="false" fetch="join" cascade="all" inverse="false" table="employee4animal">
  2.    <key column="fk_employee_id" not-null="true"/>
  3.    <many-to-any id-type="integer" meta-type="string">
  4.       <column name="fk_set_animal_kind"/>
  5.       <column name="fk_set_animal_id"/>
  6.    </many-to-any>
  7. </set>
Поля "fk_set_animal_kind", "fk_set_animal_id" будут вставлены в сгенерированную промежуточную таблицу. От этой промежуточной таблицы также как от всех остальных таблиц (для Animal, Cat, Dog) будет протянута связь (в таблицы животных будет вставлено поле fk_employee_id). Как видите, каждая запись в таблице связи содержит информацию о том какой класс нужно создать и значение идентификатора для соответствующей таблицы животных.
mysql> select * from employee4animal;
+----------------+-----------------------------------+------------------+
| fk_employee_id | fk_set_animal_kind                | fk_set_animal_id |
+----------------+-----------------------------------+------------------+
|              1 | experimental.business.animals.Dog |                1 |
|              1 | experimental.business.animals.Cat |                1 |
+----------------+-----------------------------------+------------------+
2 rows in set (0.00 sec)
А вот так будет выглядеть сгенерированная структура данных в СУБД



Последнее на сегодня: в hibernate 3 появилась новая возможность (так странно звучит, “новая” для продукта трехлетней давности) возможность организовать иерархию классов в виде стратегии “чем-то похоже на продублируй содержимое каждого класса в виде отдельной таблицы и получи в подарок, заморочки в генерации первичного ключа”. Для примера я переписываю файлы маппинга на следующие (внимание, стратегия генерации первичного ключа в таблице Animal – increment, что само по себе не хорошо).
  1. <class name="Animal" abstract="true" >
  2.    <id name="animal_id">
  3.       <generator class="increment" />
  4.    </id>
  5.    <property name="birthday" type="calendar"/>
  6.    <property name="name" type="string"/>
  7.  
  8.    <many-to-one name="owner" class="experimental.business.Employee" column="fk_employee_id"
  9.         cascade="save-update" not-null="true" />
  10. </class>
  11.  
  12. <union-subclass name="Cat" extends="Animal">
  13.    <property name="eyesColor" type="string"/>
  14. </union-subclass>
  15.  
  16. <union-subclass name="Dog" extends="Animal">
  17.    <property name="weight" type="big_decimal"/>
  18. </union-subclass>
Созданный в результате запуска скрипта таблицы будут идентичны своим братьям в “каждой сестре по серьге”. Вот только значение поля animal_id не будет иметь значения auto_increment т.к. назначается не mysql, а hibernate-ом. Чем это грозит при параллельном доступе нескольких приложений к одной БД, думаю, объяснять не стоит.

Несмотря на такие неудобства в стратегии “каждому классу по таблице” избегать ее не стоит: всегда найдется ситуация, когда именно такой способ хранения данных будет наилучшим (например, возможность искать сведения о конкретном виде животных без необходимости обращения к другим таблицам может быть выходом для высоконагруженной системы). Я тем более не говорю о классическом “унаследованном решении”. А вообще используйте joined-subclass – самое лучшее простое и надежное решение.
 Вырезка из официального мануала по hibernate.
9.2. Limitations

There are certain limitations to the "implicit polymorphism" approach to the table per concrete-class mapping strategy. There are somewhat less restrictive limitations to mappings.

The following table shows the limitations of table per concrete-class mappings, and of implicit polymorphism, in Hibernate.

Table 9.1. Features of inheritance mappings
Inheritance strategy Polymorphic many-to-one Polymorphic one-to-one Polymorphic one-to-many Polymorphic many-to-many Polymorphic load()/get() Polymorphic queries Polymorphic joins Outer join fetching
table per class-hierarchy s.get(Payment.class, id) from Payment p from Order o join o.payment p supported
table per subclass s.get(Payment.class, id) from Payment p from Order o join o.payment p supported
table per concrete-class (union-subclass) (for inverse="true" only) s.get(Payment.class, id) from Payment p from Order o join o.payment p supported
table per concrete class (implicit polymorphism) not supported not supported s.createCriteria(Payment.class).add( Restrictions.idEq(id) ).uniqueResult() from Payment p not supported not supported

Categories: Java