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)
« JSTL: Шаблоны для разработки веб-приложений в java. Часть 3 | Управление проектами вместе с JIRA. Часть 2 » |
Hibernate: отображая иерархии классов
Тема сегодняшней статьи – как отобразить иерархию классов на реляционную модель данных. Наследование – это один из столпов ООП, а раз в СУБД нет родного понятия или методики представления подобного отношения, то все что нам остается – это имитировать наследование классов различными способами. В hibernate есть три методики имитации: “вся иерархия классов в одной таблице”, “одна таблица базовому классу и каждому подклассу по таблице дополнений”, “каждому классу свою, независимую от остальных таблицу”. Продолжая наш пример с сотрудниками и отделами, представим, что у каждого сотрудника есть свое любимое животное (возможно, не одно). И создадим иерархию: Животное -> Кошка, Собака -> Тигр и т.д.Корень иерархии – класс “Animal”, вот его код и пример его маппинга:
/**
* Базовый класс для иерархии любимых зверюшек сотрудника.
*/
abstract public class Animal {
// идентификатор нашего животного
Integer animal_id;
// имя
String name;
// дата рождения
Calendar birthday;
public Animal() {
}
public Animal(String name, Calendar birthday) {
this.name = name;
this.birthday = birthday;
}
public Integer getAnimal_id() {
return animal_id;
}
public void setAnimal_id(Integer animal_id) {
this.animal_id = animal_id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Calendar getBirthday() {
return birthday;
}
public void setBirthday(Calendar birthday) {
this.birthday = birthday;
}
}
/**
* Кошка
*/
public class Cat extends Animal {
// цвет глаз кошки
String eyesColor;
public Cat() {
}
public Cat(String name, Calendar birthday, String eyesColor) {
super(name, birthday);
this.eyesColor = eyesColor;
}
public String getEyesColor() {
return eyesColor;
}
public void setEyesColor(String eyesColor) {
this.eyesColor = eyesColor;
}
}
/**
* Собака
*/
public class Dog extends Animal {
// вес собаки
BigDecimal weight;
public Dog() {
}
public Dog(String name, Calendar birthday, BigDecimal weight) {
super(name, birthday);
this.weight = weight;
}
public BigDecimal getWeight() {
return weight;
}
public void setWeight(BigDecimal weight) {
this.weight = weight;
}
}
/**
* Тигр, мда ... домашнее животное
*/
public class Tiger extends Cat {
// количество съеденных сотрудников
Integer countEatenExployees;
public Tiger() {
}
public Tiger(String name, Calendar birthday, String eyesColor, Integer countEatenExployees) {
super(name, birthday, eyesColor);
this.countEatenExployees = countEatenExployees;
}
public Integer getCountEatenExployees() {
return countEatenExployees;
}
public void setCountEatenExployees(Integer countEatenExployees) {
this.countEatenExployees = countEatenExployees;
}
}
<hibernate-mapping package="experimental.business.animals">
<class name="Animal" abstract="true">
<id name="animal_id">
<generator class="native" />
</id>
<property name="birthday" type="calendar"/>
<property name="name" type="string"/>
<subclass name="Dog">
<!-- ..... бла-бла-бла -->
</subclass>
</class>
<subclass name="Cat" extends="Animal">
<!-- ..... бла-бла-бла -->
<subclass name="Tiger">
<!-- ..... бла-бла-бла -->
</subclass>
</subclass>
</hibernate-mapping>
Я, однако, предпочитаю другой вариант, когда каждому классу наследнику дается свой отдельный *.hbm.xml файл (особенно, если иерархия большая или велико количество полей в каждом из классов).
Однако в этом примере чего-то не хватает. Давайте задумаемся, раз у на все классы будут храниться в одной таблице, то эта таблица должна быть общим множеством всех полей и в базовом классе и во всех классах наследниках – просто для некоторых записей значение набора полей будет пустым. Однако, остается открытым вопрос как hibernate на стадии чтения данных из СУБД сможет узнать что вот эта, конкретная, запись должна быть отображена в виде класса Cat или Dog. Существующих полей для принятия решения совершенно недостаточно: мало ли по какой причине поля будут равны null. Лучшим выходом из сложившейся ситуации будет создание специального маркерного поля. Его называют дискриминатором) в котором будет храниться имя того класса который нужно создать. Вообще, хранить можно, что угодно, даже не полные имена классов, а цифры или короткие коды (C,D,T), только это неудобочитаемо, а экономить на спичках (используя короткие коды) – глупо. Итак, теперь пример обновленных файлов маппинга уже с учетом дискриминатора (точно такое же решение используется и в других ORM frameworks, например, ibatis):
<class name="Animal" abstract="true" discriminator-value="Animal">
<id name="animal_id">
<generator class="native" />
</id>
<discriminator column="DSK" type="string" length="30" not-null="true"/>
<property name="birthday" type="calendar"/>
<property name="name" type="string"/>
</class>
<hibernate-mapping package="experimental.business.animals">
<subclass name="Cat" extends="Animal" discriminator-value="Cat">
<property name="eyesColor" type="string"/>
</subclass>
</hibernate-mapping>
<hibernate-mapping package="experimental.business.animals">
<subclass name="Dog" extends="Animal" discriminator-value="Dog">
<property name="weight" type="big_decimal"/>
</subclass>
</hibernate-mapping>
<hibernate-mapping package="experimental.business.animals">
<subclass name="Tiger" extends="Cat" discriminator-value="Tiger">
<property name="countEatenExployees" type="int"/>
</subclass>
</hibernate-mapping>
CREATE TABLE `animal` (
`animal_id` int(11) NOT NULL AUTO_INCREMENT,
`DSK` varchar(30) NOT NULL,
`birthday` datetime DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`eyesColor` varchar(255) DEFAULT NULL,
`weight` decimal(19,2) DEFAULT NULL,
`countEatenExployees` int(11) DEFAULT NULL,
PRIMARY KEY (`animal_id`)
) ENGINE=InnoDB
Теперь какие есть подводные камни при работе с этими полиморфными классами?
Давайте посмотрим на следующий фрагмент кода:
Animal ani_1 = (Animal) ses.load(Animal.class, 1);
System.out.println("is it Animal ? " + (ani_1 instanceof Animal) );
System.out.println("is it Cat ? " + (ani_1 instanceof Cat) );
System.out.println("is it Dog ? " + (ani_1 instanceof Dog) );
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
Cat murka_2 = (Cat) ses.load(Cat.class , 1);
Dog bobik_2 = (Dog) ses.load(Dog.class , 1);
System.out.println("murka_2 = " + murka_2);
System.out.println("bobik_2 = " + bobik_2);
На этом все про способ организации наследования “все классы иерархии в одной таблице”. Второй вид организации наследования предполагает существование одной базовой таблицы (для корня нашей иерархии – класса Animal). Что же касается остальных классов наследников, то для каждого из них будет создана своя отдельная таблица. Но в этой таблице будут содержаться лишь те поля, которые являются специфическими для этого класса. Естественно, что от каждого из таких подчиненных классов к клавному классу/таблице должна быть протянута на уровне СУБД связь “один-ко-многим” (все это делается hibernate-ом прозрачно и ни о чем не нужно беспокоиться, почти …).
Для демонстрации нового вида организации наследования я не будут никак исправлять код java-классов. Все изменения будут только в файлах маппинга:
<class name="Animal" abstract="true" >
<id name="animal_id">
<generator class="native" />
</id>
<property name="birthday" type="calendar"/>
<property name="name" type="string"/>
</class>
<joined-subclass name="Cat" extends="Animal" >
<key column="fk_animal_id" not-null="true" />
<property name="eyesColor" type="string"/>
</joined-subclass>
<joined-subclass name="Dog" extends="Animal">
<key column="fk_animal_id" not-null="true" />
<property name="weight" type="big_decimal"/>
</joined-subclass>
<joined-subclass name="Tiger" extends="Cat">
<key column="fk_cat_id" not-null="true" />
<property name="countEatenExployees" type="int"/>
</joined-subclass>
Теперь смотрим на изменения, которые произошли в структуре базы данных:
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)
Теперь попробуем вернуться к изначальному заданию и созданную иерархию домашних животных (ага, особенно тигру) привяжем к сотруднику. Связь будет “один-ко-многим” и двунаправленная.
А вот пример использования такой связи:
ses.beginTransaction();
Calendar birthMurka = new GregorianCalendar(2007, 1, 1);
Cat murka = new Cat("Murka", birthMurka, "red");
Dog bobik = new Dog("Bobik", new GregorianCalendar(2001, 1, 1), new BigDecimal(120.0));
Tiger bazz = new Tiger("Bazz", new GregorianCalendar(1978, 1, 1), "green", 12);
Department designers = new Department("designers");
Employee lena = new Employee("lena");
lena.setDepartment(designers);
designers.getEmployies().add(lena);
lena.getAnimals().add(murka);
lena.getAnimals().add(bobik);
lena.getAnimals().add(bazz);
murka.setOwner(lena);
bobik.setOwner(lena);
bazz.setOwner(lena);
ses.saveOrUpdate(lena);
ses.getTransaction().commit();
System.out.println("------------------------------------------");
ses = factory.openSession();
ses.beginTransaction();
Employee lena_2 = (Employee) ses.load(Employee.class, lena.getEmployee_id());
ses.delete(lena_2);
ses.getTransaction().commit();
<class name="Animal" abstract="true" >
<id name="animal_id">
<generator class="native" />
</id>
<property name="birthday" type="calendar"/>
<property name="name" type="string"/>
<many-to-one name="owner" class="experimental.business.Employee" column="fk_employee_id"
cascade="save-update" not-null="true" />
</class>
<class name="Employee" dynamic-insert="true" dynamic-update="true" >
<id name="employee_id">
<generator class="native"/>
</id>
<property name="fio"/>
<many-to-one name="department" class="Department" column="fk_department_id" not-null="true"
cascade="save-update"/>
<property name="bio" lazy="true" type="text"/>
<set name="animals" lazy="false" fetch="join" cascade="all" inverse="true">
<key column="fk_employee_id" not-null="true" />
<one-to-many class="experimental.business.animals.Animal" />
</set>
</class>
public class Employee {
Integer employee_id;
String fio;
Department department;
Integer uo;
String bio;
// список домаших животных сотрудника
Set <Animal> animals = new HashSet<Animal>();
public Set<Animal> getAnimals() {
return animals;
}
public void setAnimals(Set<Animal> animals) {
this.animals = animals;
}
// ...
}
/**
* Базовый класс для иерархии любимых зверюшек сотрудника.
*/
abstract public class Animal {
// идентификатор нашего животного
Integer animal_id;
// имя
String name;
// дата рождения
Calendar birthday;
Employee owner;
public Employee getOwner() {
return owner;
}
public void setOwner(Employee owner) {
this.owner = owner;
}
// ...
}
Начнем с того, что полностью перепишем файлы маппинга (сами классы с иерархией животных я трогать не буду):
<class name="Cat">
<id name="animal_id">
<generator class="native"/>
</id>
<property name="birthday" type="calendar"/>
<property name="name" type="string"/>
<many-to-one name="owner" class="experimental.business.Employee" column="fk_employee_id"
cascade="save-update" not-null="true"/>
<property name="eyesColor" type="string"/>
</class>
<class name="Dog">
<id name="animal_id">
<generator class="native"/>
</id>
<property name="birthday" type="calendar"/>
<property name="name" type="string"/>
<many-to-one name="owner" class="experimental.business.Employee" column="fk_employee_id"
cascade="save-update" not-null="true"/>
<property name="weight" type="big_decimal"/>
</class>
<class name="Tiger">
<id name="animal_id">
<generator class="native"/>
</id>
<property name="birthday" type="calendar"/>
<property name="name" type="string"/>
<many-to-one name="owner" class="experimental.business.Employee" column="fk_employee_id"
cascade="save-update" not-null="true"/>
<property name="eyesColor" type="string"/>
<property name="countEatenExployees" type="int"/>
</class>
Смотрим изменения в маппинге для сотрудника:
<class name="Employee" dynamic-insert="true" dynamic-update="true" >
<id name="employee_id">
<generator class="native"/>
</id>
<property name="fio"/>
<many-to-one name="department" class="Department" column="fk_department_id" not-null="true"
cascade="save-update"/>
<property name="bio" lazy="true" type="text"/>
<one-to-one name="car" class="Car" cascade="all"/>
<any meta-type="string" id-type="integer" name="friend" cascade=”save-update”>
<column name="fk_animal_kind" />
<column name="fk_animal_id" />
</any>
</class>
Можно записать “переключатель типов” и подобным образом (в качестве ключей используются не имена классов, а произвольные строки).
<any meta-type="string" id-type="integer" name="friend" cascade=”save-update”>
<meta-value value="C" class="Cat"/>
<meta-value value="D" class="Dog"/>
<meta-value value="T" class="Tiger"/>
<column name="fk_animal_kind" />
<column name="fk_animal_id" />
</any>
CREATE TABLE `employee` (
`employee_id` int(11) NOT NULL AUTO_INCREMENT,
`fio` varchar(255) DEFAULT NULL,
`fk_department_id` int(11) NOT NULL,
`bio` text,
`fk_animal_kind` varchar(255) DEFAULT NULL,
`fk_animal_id` int(11) DEFAULT NULL,
PRIMARY KEY (`employee_id`),
KEY `FK4AFD4ACE28F13B88` (`fk_department_id`),
CONSTRAINT `FK4AFD4ACE28F13B88` FOREIGN KEY (`fk_department_id`) REFERENCES `department` (`department_id`)
) 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)
public class Employee {
Integer employee_id;
String fio;
Department department;
Integer uo;
String bio;
// лучший друг сотрудника (демонстрация связи any)
Animal friend;
public Animal getFriend() {
return friend;
}
public void setFriend(Animal friend) {
this.friend = friend;
}
// ...
}
Department designers = new Department("designers");
Employee lena = new Employee("lena");
lena.setDepartment(designers);
designers.getEmployies().add(lena);
bazz.setOwner(lena);
lena.setFriend(bazz);
ses.saveOrUpdate(bazz);
ses.saveOrUpdate(lena);
ses.getTransaction().commit();
Вот так будет выглядеть обновленный маппинг для сотрудника:
<set name="animals" lazy="false" fetch="join" cascade="all" inverse="false" table="employee4animal">
<key column="fk_employee_id" not-null="true"/>
<many-to-any id-type="integer" meta-type="string">
<column name="fk_set_animal_kind"/>
<column name="fk_set_animal_id"/>
</many-to-any>
</set>
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, что само по себе не хорошо).
<class name="Animal" abstract="true" >
<id name="animal_id">
<generator class="increment" />
</id>
<property name="birthday" type="calendar"/>
<property name="name" type="string"/>
<many-to-one name="owner" class="experimental.business.Employee" column="fk_employee_id"
cascade="save-update" not-null="true" />
</class>
<union-subclass name="Cat" extends="Animal">
<property name="eyesColor" type="string"/>
</union-subclass>
<union-subclass name="Dog" extends="Animal">
<property name="weight" type="big_decimal"/>
</union-subclass>
Несмотря на такие неудобства в стратегии “каждому классу по таблице” избегать ее не стоит: всегда найдется ситуация, когда именно такой способ хранения данных будет наилучшим (например, возможность искать сведения о конкретном виде животных без необходимости обращения к другим таблицам может быть выходом для высоконагруженной системы). Я тем более не говорю о классическом “унаследованном решении”. А вообще используйте 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
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) | 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 |
« JSTL: Шаблоны для разработки веб-приложений в java. Часть 3 | Управление проектами вместе с JIRA. Часть 2 » |