mysql> select * from employee; +-------------+------+------------------+------+ | employee_id | fio | fk_department_id | uo | +-------------+------+------------------+------+ | 1 | tom | 1 | NULL | | 2 | ron | 1 | NULL | | 3 | jim | 1 | NULL | +-------------+------+------------------+------+ 3 rows in set (0.00 sec)
« Hibernate: каскадные обновления, инверсия отношений и прочая и прочая | Системы управления версиями для программистов и не только. Часть 5 » |
Hibernate: Set-ы, bag-и и все, все, все
Продолжаю рассказывать про hibernate и ассоциации. Сегодняшний материал продолжает прошлую статью, так что все примеры предполагаются над моделью данных: отдел & сотрудник. При организации связи один-ко-многим, мы должны внутрь класса Отдел поместить контейнер, хранящий список подчиненных объектов. Есть несколько вариантов того, каким может быть этот контейнер и выбор является не настолько тривиальным.Начнем с того, что вспомним основные виды контейнеров из java collections api. Есть базовый интерфейс Collection, от которого производны интерфейсы Set и List. Первый из них представляет собой неупорядоченную коллекцию элементов (дубляж элементов в терминах equals запрещен).
Интерфейс же List не накладывает таких ограничений: элементы могут дублироваться, и есть способ пронумеровать элементы, обращаться к ним по порядку.
Для тех, кому хочется пользоваться автоматическим отбрасыванием дублей как в set, но не нравится то, что порядок элементов произволен, можно использовать либо LinkedHashSet (порядок элементов множества диктуется порядком их добавления). Либо использовать SortedSet (это также интерфейс, но все классы, реализующие его, должны упорядочивать элементы множества согласно некоторому правилу). Из конкретных реализаций SortedSet-а, можно назвать класс TreeSet. В котором элементы сортируются либо в соответствии с их натуральным порядком (классы элементов должны реализовать интерфейс Comparable), либо с помощью некоторого, явно заданного объекта-сравнителя (Comparator).
Немного в стороне от этой стройной иерархии находится интерфейс Map (карта, ассоциативный массив), в котором для обращения к элементам мы используем не порядковый номер, а произвольный объект-ключ (естественно, что дублирование значения ключа не допустимо). Для тех, кто хочет организовать проход по всему содержимому Map и хочется делать это в строгом порядке, пригодится класс LinkedHashMap. А если вам нужна автоматическая сортировка ключей карты по некоторому правилу, то вам пригодится интерфейс SortedMap и его конкретная реализация TreeMap (сортировка идет либо с помощью Comparator-а, либо с помощью натурального упорядочения).
Знать эти интерфейсы нужно обязательно и еще раз обязательно. Дело в том, что в hibernate есть конкретные классы, реализующие данные интерфейсы. Здесь же, типовая ошибка при работе с коллекциями, заключается в том, что вы указываете названия конкретных классов в декларациях методов set&get. Например:
public class Department {
HashSet<Employee> employies = new HashSet<Employee>();
public HashSet<Employee> getEmployies() {
return employies;
}
public void setEmployies(HashSet<Employee> employies) {
this.employies = employies;
}
}
Exception in thread "main" org.hibernate.PropertyAccessException: IllegalArgumentException occurred while calling setter of experimental.business.Department.employiesДело в том, что при операциях загрузки и сохранения, hibernate создает коллекцию (собственную реализацию коллекции) и пытается ее целиком присвоить вашему полю через метод setEmployies. Так для set-а, это будет класс org.hibernate.collection.PersistentSet, непосредственно реализующий интерфейс Set и никак не связанный по линии наследования с классом HashSet. Чем закончится попытка присвоить HashSet-у значение PersistentSet, вы, я думаю, уже догадались.
Теперь давайте перейдем к рассмотрению различных видов коллекций. Старый добрый set.
<set name="employies" cascade="all" inverse="true">
<key column="fk_department_id" />
<one-to-many class="Employee" />
</set>
CREATE TABLE `employee` (
`employee_id` int(11) NOT NULL AUTO_INCREMENT,
`fio` varchar(255) DEFAULT NULL,
`fk_department_id` int(11) NOT NULL,
PRIMARY KEY (`employee_id`),
KEY `FK4AFD4ACE28F13B88` (`fk_department_id`),
CONSTRAINT `FK4AFD4ACE28F13B88` FOREIGN KEY (`fk_department_id`) REFERENCES `department` (`department_id`)
) ENGINE=InnoDB
Теперь меня не устраивает то, что сотрудники в отделе представлены как неупорядоченное множество. Я хочу, чтобы все они были размещены согласно некоторому порядку. К сожалению, использовать коллекцию set мы больше не можем – нужно перейти к list и поверьте, что нас ждет несколько неприятных сюрпризов.
Прежде всего, я изменил код класса Department, везде заменив тип Set на List.
/**
* Отдел
*/
public class Department {
Integer department_id;
String caption;
List<Employee> employies = new ArrayList<Employee>();
public Department() {
}
public Department(String caption) {
this.caption = caption;
}
public Integer getDepartment_id() {
return department_id;
}
public void setDepartment_id(Integer department_id) {
this.department_id = department_id;
}
public String getCaption() {
return caption;
}
public void setCaption(String caption) {
this.caption = caption;
}
public List<Employee> getEmployies() {
return employies;
}
public void setEmployies(List<Employee> employies) {
this.employies = employies;
}
}
<list name="employies" cascade="all" inverse="true" >
<key column="fk_department_id" />
<list-index column="uo" />
<one-to-many class="Employee" />
</list>
Это число должно быть целым и при генерации структуры данных будет выглядеть так (как видите, в списке полей есть поле uo - UserOrder):
CREATE TABLE `employee` (
`employee_id` int(11) NOT NULL AUTO_INCREMENT,
`fio` varchar(255) DEFAULT NULL,
`fk_department_id` int(11) NOT NULL,
`uo` 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
ses.beginTransaction();
Employee jim = new Employee("jim");
Employee tom = new Employee("tom");
Employee ron = new Employee("ron");
Department managers = new Department("managers");
jim.setDepartment(managers);
tom.setDepartment(managers);
ron.setDepartment(managers);
managers.getEmployies().add(tom);
managers.getEmployies().add(ron);
managers.getEmployies().add(jim);
ses.saveOrUpdate(managers);
ses.getTransaction().commit();
Исправления в файле Отдела:
<list name="employies" cascade="all" >
<key column="fk_department_id" />
<list-index column="uo" />
<one-to-many class="Employee" />
</list>
mysql> select * from employee; +-------------+------+------------------+------+ | employee_id | fio | fk_department_id | uo | +-------------+------+------------------+------+ | 1 | tom | 1 | 0 | | 2 | ron | 1 | 1 | | 3 | jim | 1 | 2 | +-------------+------+------------------+------+ 3 rows in set (0.00 sec)
<property name="uo" />
ses.beginTransaction();
Employee jim = new Employee("jim");
Employee tom = new Employee("tom");
Employee ron = new Employee("ron");
Department managers = new Department("managers");
jim.setDepartment(managers);
tom.setDepartment(managers);
ron.setDepartment(managers);
managers.getEmployies().add(tom);
managers.getEmployies().add(ron);
managers.getEmployies().add(jim);
ses.saveOrUpdate(jim);
ses.getTransaction().commit();
System.out.println("------------------------------------------");
ses = factory.openSession();
ses.beginTransaction();
Department managers_2 = (Department) ses.load(Department.class, 1);
// такие изменения не отслеживаются (этот ужасный кусок кода
// выполняет перестановку местами первого и последнего элемента списка) если я,
// установил модификатор inverse=true
// managers_2.getEmployies().add(managers_2.getEmployies().remove(0));
Employee jim_2 = managers_2.getEmployies().get(1);
// попробуем явно задать порядковый номер сотруднику в отделе
jim_2.setUo(4);
mysql> select * from employee; +-------------+------+------+------------------+ | employee_id | fio | uo | fk_department_id | +-------------+------+------+------------------+ | 1 | tom | 0 | 1 | | 2 | ron | 4 | 1 | | 3 | jim | 2 | 1 | +-------------+------+------+------------------+ 3 rows in set (0.01 sec)
Как видите, фактически список из дочерних к отделу сотрудников состоит из пяти элементов, просто номеров 1, 3 нет (они равны null). Так что играться подобным образом и задавать порядковый номер сотрудника, чтобы он был использован при чтении коллекции, не стоит: проблем больше чем преимуществ. С другой стороны, когда-то давно я создал свой маленький велосипедик решающий эту проблему, но об этом в другой раз.
Теперь попробуем создать коллекцию вида bag. Физически эта коллекция представляется в классе отдела коллекций List (точно также как и list), вот только никакого автоматического упорядочения при сохранении записей выполняться не будет.
<hibernate-mapping package="experimental.business">
<class name="Department" dynamic-insert="true" dynamic-update="true">
<id name="department_id">
<generator class="native" />
</id>
<property name="caption" />
<bag name="employies" cascade="all" inverse="true" >
<key column="fk_department_id" />
<one-to-many class="Employee" />
</bag>
</class>
</hibernate-mapping>
Сначала я переписываю код класса Отдел, меняю тип поля employees на Array:
Employee [] employies;
<array name="employies" cascade="all" inverse="true" >
<key column="fk_department_id" />
<list-index column="uo" />
<one-to-many class="Employee" />
</array>
Еще одна тривиальная и не очень полезная структура данных – массив примитивов. Собственно я ей никогда не пользовался, т.к. с появлением jdk 1.5 проблема появился boxing-outboxing.
Теперь сложное и интересное – карта. Мы можем в состав отдела ввести не просто список сотрудников, но Map. В котором, как роль ключа, так и роль значения могут играть и произвольные (не Domain-объекты), так и замапленные на hibernate классы. Для примера я создам ассоциацию: дата рождения (в виде полноценного java.util.Date) – объект сотрудник.
public class Department {
Integer department_id;
String caption;
Map<Date, Employee> employies = new HashMap<Date, Employee>();
public Department() {
}
public Department(String caption) {
this.caption = caption;
}
public Integer getDepartment_id() {
return department_id;
}
public void setDepartment_id(Integer department_id) {
this.department_id = department_id;
}
public String getCaption() {
return caption;
}
public void setCaption(String caption) {
this.caption = caption;
}
public Map<Date, Employee> getEmployies() {
return employies;
}
public void setEmployies(Map<Date, Employee> employies) {
this.employies = employies;
}
}
<hibernate-mapping package="experimental.business">
<class name="Department" dynamic-insert="true" dynamic-update="true">
<id name="department_id">
<generator class="native" />
</id>
<property name="caption" />
<map name="employies" cascade="all" >
<key column="fk_department_id" />
<map-key type="timestamp" column="birthdate" />
<one-to-many class="Employee" />
</map>
</class>
</hibernate-mapping>
ses.beginTransaction();
Employee jim = new Employee("jim");
Employee tom = new Employee("tom");
Employee ron = new Employee("ron");
Department managers = new Department("managers");
jim.setDepartment(managers);
tom.setDepartment(managers);
ron.setDepartment(managers);
Calendar bd_jim = new GregorianCalendar(2006, 1, 1);
Calendar bd_tom = new GregorianCalendar(2002, 6, 8);
Calendar bd_ron = new GregorianCalendar(2001, 11, 16);
managers.getEmployies().put(bd_tom.getTime(), tom);
managers.getEmployies().put(bd_ron.getTime(), ron);
managers.getEmployies().put(bd_jim.getTime(), jim);
ses.saveOrUpdate(jim);
ses.getTransaction().commit();
// ---------------------------
ses = factory.openSession();
ses.beginTransaction();
Department managers_2 = (Department) ses.load(Department.class, 1);
Map<Date,Employee> employies = managers_2.getEmployies();
for (Date date : employies.keySet()) {
Employee emp = employies.get(date);
System.out.println("date = " + date + " " + emp.getFio());
}
ses.getTransaction().commit();
CREATE TABLE `employee` (
`employee_id` int(11) NOT NULL AUTO_INCREMENT,
`fio` varchar(255) DEFAULT NULL,
`fk_department_id` int(11) NOT NULL,
`birthdate` datetime DEFAULT NULL,
PRIMARY KEY (`employee_id`),
KEY `FK4AFD4ACE28F13B88` (`fk_department_id`),
CONSTRAINT `FK4AFD4ACE28F13B88` FOREIGN KEY (`fk_department_id`) REFERENCES `department` (`department_id`)
) ENGINE=InnoDB
mysql> select * from employee; +-------------+------+------------------+---------------------+ | employee_id | fio | fk_department_id | birthdate | +-------------+------+------------------+---------------------+ | 1 | tom | 1 | 2002-07-08 00:00:00 | | 2 | ron | 1 | 2001-12-16 00:00:00 | | 3 | jim | 1 | 2006-02-01 00:00:00 | +-------------+------+------------------+---------------------+ 3 rows in set (0.00 sec)
Сначала я создаю новый класс EmployeeKey:
/**
* Класс карточки сотрудника.
*/
public class EmployeeKey {
Integer id;
String code;
public EmployeeKey() {
}
public EmployeeKey(String code) {
this.code = code;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="experimental.business">
<class name="EmployeeKey">
<id name="id" >
<generator class="native" />
</id>
<property name="code" />
</class>
</hibernate-mapping>
public class Department {
Integer department_id;
String caption;
Map<EmployeeKey, Employee> employies = new HashMap<EmployeeKey, Employee>();
public Department() {
}
public Department(String caption) {
this.caption = caption;
}
public Integer getDepartment_id() {
return department_id;
}
public void setDepartment_id(Integer department_id) {
this.department_id = department_id;
}
public String getCaption() {
return caption;
}
public void setCaption(String caption) {
this.caption = caption;
}
public Map<EmployeeKey, Employee> getEmployies() {
return employies;
}
public void setEmployies(Map<EmployeeKey, Employee> employies) {
this.employies = employies;
}
}
<hibernate-mapping package="experimental.business">
<class name="Department" dynamic-insert="true" dynamic-update="true">
<id name="department_id">
<generator class="native" />
</id>
<property name="caption" />
<map name="employies" cascade="all" >
<key column="fk_department_id" />
<map-key-many-to-many class="EmployeeKey" />
<one-to-many class="Employee" />
</map>
</class>
</hibernate-mapping>
Теперь пример кода тестирующего измененную структуру данных:
ses.beginTransaction();
Employee jim = new Employee("jim");
Employee tom = new Employee("tom");
Employee ron = new Employee("ron");
Department managers = new Department("managers");
jim.setDepartment(managers);
tom.setDepartment(managers);
ron.setDepartment(managers);
EmployeeKey key_tom = new EmployeeKey("abba-tom");
managers.getEmployies().put(key_tom, tom);
EmployeeKey key_ron = new EmployeeKey("barr-ron");
managers.getEmployies().put(key_ron, ron);
EmployeeKey key_jim = new EmployeeKey("terr-jim");
managers.getEmployies().put(key_jim, jim);
ses.saveOrUpdate(jim);
// сохраняем все ключи, на них не действует запись cascade="all"
ses.saveOrUpdate(key_jim);
ses.saveOrUpdate(key_ron);
ses.saveOrUpdate(key_tom);
ses.getTransaction().commit();
System.out.println("------------------------------------------");
ses = factory.openSession();
ses.beginTransaction();
Department managers_2 = (Department) ses.load(Department.class, 1);
Map<EmployeeKey, Employee> employies = managers_2.getEmployies();
for (EmployeeKey key : employies.keySet()) {
Employee emp = employies.get(key);
System.out.println("key = " + key.getCode() + " " + emp.getFio());
}
ses.getTransaction().commit();
Последнее о чем я сегодня расскажу - это сортировка коллекций при загрузке. В hibernate есть два метода как можно сортировать элементы коллекций (не всех очевидно, так для list сортировка не имеет фактического смысла из-за того, что упорядочение идет по индексу). Однако set и map можно упорядочить (у элемента bag, idbag также нет подобной функциональности).
Количество действий которое нужно выполнить для того чтобы отсортировать коллекцию сотрудников отдела достаточно велико (собственно говоря, я никогда не использовал функцию сортировки при работе с Entity или композитными элемента, только когда использовал коллекцию из примитивов: чисел, строк).
Начнем с того, что есть два варианта упорядочения: на стадии запроса данных из БД и средствами именно БД, и второй вариант (более гибкий) после того как объекты были загружены в память, то они сортируется классическим Collections.sort (только не спрашивайте меня, почему столь простую операцию нужно возложить на hibernate, читай, жестко "забить в конфиге", а не выполнить "ручками").
Начнем с переработки класса Отдел, нужно изменить тип коллекции сотрудников с Set на SortedSet. Теперь подумаем, раз я создал реализацию SortedSet-а в виде TreeSet, то при добавлении в отдел сотрудников (начиная со второго, если быть точным) эти сотрудники начинают сортироваться (интересно как, если наш оригинальный класс не реализовывал интерфейс Comparable). Наиболее простым выходом из ситуации будет создать специальный класс "сортировальщик абы-как" (его код я покажу чуть позже):
public class Department {
Integer department_id;
String caption;
SortedSet<Employee> employies = new TreeSet<Employee>(new SortEmployeeAsIs());
public Department() {
}
public Department(String caption) {
this.caption = caption;
}
public Integer getDepartment_id() {
return department_id;
}
public void setDepartment_id(Integer department_id) {
this.department_id = department_id;
}
public String getCaption() {
return caption;
}
public void setCaption(String caption) {
this.caption = caption;
}
public SortedSet<Employee> getEmployies() {
return employies;
}
public void setEmployies(SortedSet<Employee> employies) {
this.employies = employies;
}
}
/**
* Сортируем сотрудников нашего отдела, по их hashCode.
* Ну и что здесь такого странного, в любом случае это работает только на первом этапе
* до загрузки отдела из БД
*/
public class SortEmployeeAsIs implements Comparator {
public int compare(Object o1, Object o2) {
if (o1 == null) return -1;
if (o2 == null) return +1;
return o1.hashCode() - o2.hashCode();
}
}
<hibernate-mapping package="experimental.business">
<class name="Department" dynamic-insert="true" dynamic-update="true">
<id name="department_id">
<generator class="native"/>
</id>
<property name="caption"/>
<set name="employies" cascade="all" inverse="true" sort="experimental.business.etc.ComparatorByFIO">
<key column="fk_department_id"/>
<one-to-many class="Employee"/>
</set>
</class>
</hibernate-mapping>
/**
* Сравнение Сотрудников выполняется на основании их ФИО
*/
public class ComparatorByFIO implements Comparator {
public int compare(Object o1, Object o2) {
Employee u1 = (Employee)o1;
Employee u2 = (Employee)o2;
return u1.getFio().compareTo(u2.getFio());
}
}
<set name="employies" cascade="all" inverse="true" order-by="fio">
<key column="fk_department_id"/>
<one-to-many class="Employee"/>
</set>
На этом все.
« Hibernate: каскадные обновления, инверсия отношений и прочая и прочая | Системы управления версиями для программистов и не только. Часть 5 » |