Hibernate: Set-ы, bag-и и все, все, все

May 14, 2008

Продолжаю рассказывать про 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. Например:
  1. public class Department {
  2.     HashSet<Employee> employies = new HashSet<Employee>();
  3.  
  4.     public HashSet<Employee> getEmployies() {
  5.         return employies;
  6.     }
  7.  
  8.     public void setEmployies(HashSet<Employee> employies) {
  9.         this.employies = employies;
  10.     }
  11. }
Этот код приведет примерно к такой ошибке:
 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.
  1. <set name="employies" cascade="all" inverse="true">
  2.   <key column="fk_department_id"  />
  3.   <one-to-many class="Employee"  />
  4. </set>
Приводит к генерации на уровне базы следующей таблицы:
  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.   PRIMARY KEY (`employee_id`),
  6.   KEY `FK4AFD4ACE28F13B88` (`fk_department_id`),
  7.   CONSTRAINT `FK4AFD4ACE28F13B88` FOREIGN KEY (`fk_department_id`) REFERENCES `department` (`department_id`)
  8. ) ENGINE=InnoDB
Мы просто добавили к подчиненной таблице Сотрудников, еще одно поле играющее роль внешнего ключа и ссылающегося на главную таблицу Отдел.

Теперь меня не устраивает то, что сотрудники в отделе представлены как неупорядоченное множество. Я хочу, чтобы все они были размещены согласно некоторому порядку. К сожалению, использовать коллекцию set мы больше не можем – нужно перейти к list и поверьте, что нас ждет несколько неприятных сюрпризов.

Прежде всего, я изменил код класса Department, везде заменив тип Set на List.
  1. /**
  2.  * Отдел
  3.  */
  4. public class Department {
  5.     Integer department_id;
  6.     String caption;
  7.     List<Employee> employies = new ArrayList<Employee>();
  8.  
  9.     public Department() {
  10.     }
  11.  
  12.     public Department(String caption) {
  13.         this.caption = caption;
  14.     }
  15.  
  16.     public Integer getDepartment_id() {
  17.         return department_id;
  18.     }
  19.  
  20.     public void setDepartment_id(Integer department_id) {
  21.         this.department_id = department_id;
  22.     }
  23.  
  24.     public String getCaption() {
  25.         return caption;
  26.     }
  27.  
  28.     public void setCaption(String caption) {
  29.         this.caption = caption;
  30.     }
  31.  
  32.     public List<Employee> getEmployies() {
  33.         return employies;
  34.     }
  35.  
  36.     public void setEmployies(List<Employee> employies) {
  37.         this.employies = employies;
  38.     }
  39. }
Теперь, я изменяю код файла отображения Отдела:
  1. <list name="employies" cascade="all"  inverse="true" >
  2.   <key column="fk_department_id"  />
  3.   <list-index column="uo" />
  4.   <one-to-many class="Employee"  />
  5. </list>
Обратите внимание на то, что внутрь тега list был добавлен элемент его назначение – задать колонку, хранящую число – порядковый номер сотрудника в отделе.

Это число должно быть целым и при генерации структуры данных будет выглядеть так (как видите, в списке полей есть поле uo - UserOrder):
  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.   `uo` int(11) DEFAULT NULL,
  6.   PRIMARY KEY (`employee_id`),
  7.   KEY `FK4AFD4ACE28F13B88` (`fk_department_id`),
  8.   CONSTRAINT `FK4AFD4ACE28F13B88` FOREIGN KEY (`fk_department_id`) REFERENCES `department` (`department_id`)
  9. ) ENGINE=InnoDB
Запускаю приложение:
  1. ses.beginTransaction();
  2.  
  3. Employee jim = new Employee("jim");
  4. Employee tom = new Employee("tom");
  5. Employee ron = new Employee("ron");
  6. Department managers = new Department("managers");
  7.  
  8. jim.setDepartment(managers);
  9. tom.setDepartment(managers);
  10. ron.setDepartment(managers);
  11.  
  12. managers.getEmployies().add(tom);
  13. managers.getEmployies().add(ron);
  14. managers.getEmployies().add(jim);
  15.  
  16. ses.saveOrUpdate(managers);
  17.  
  18. ses.getTransaction().commit();
Ошибок не выявлено, неужели можно радоваться? Нетушки, давайте посмотрим, что у нас хранится в базе данных:
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)
Сотрудники в отдел номер 1 были добавлены, вот только значение поля uo – null. Почему? Да все дело потому, что при сохранении отдела управление этой операцией возложено на сотрудника, а не на отдел. Вспоминайте, ради чего мы прошлый раз мучались и вводили понятие inverse. Когда я пометил отношение list этим атрибутом, то при сохранении содержимого отдела, отдел условно сказал сотруднику: парень сохраняйся сам как знаешь. Вот наш Джим, Том и Рон сохранились, естественно не зная что нужно выставить какой-то номер в полей uo. Решение в лоб – отказаться от inverse=”true”. Пробуем:

Исправления в файле Отдела:
  1. <list name="employies" cascade="all"  >
  2.   <key column="fk_department_id"  />
  3.   <list-index column="uo" />
  4.   <one-to-many class="Employee"  />
  5. </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)
Да, порядковые номера появились, но с отказом от inverse вернулись все проблемы связанные с лишними запросами на изменение информации и невозможность выполнить удаление сотрудника, отдела не получив кучу исключений. Стоит оно того? По моему нет. Как решить проблему, кроме как отказавшись от list в пользу set? Ну ... можно воспользоваться таким типом коллекции как bag (правда там также возможности задать порядок следования элементов списка). Однако, перед этим давайте немного поиграем. Так добавив к классу Employee поле uo, то самое поле, которое у нас помечено как хранящее порядковый номер элемента. Затем изменим маппинг класса Сотрудник, добавив там новое поле:
  1. <property name="uo" />
И теперь попробуем запустить следующий код:
  1. ses.beginTransaction();
  2.  
  3. Employee jim = new Employee("jim");
  4. Employee tom = new Employee("tom");
  5. Employee ron = new Employee("ron");
  6. Department managers = new Department("managers");
  7.  
  8. jim.setDepartment(managers);
  9. tom.setDepartment(managers);
  10. ron.setDepartment(managers);
  11.  
  12. managers.getEmployies().add(tom);
  13. managers.getEmployies().add(ron);
  14. managers.getEmployies().add(jim);
  15.  
  16. ses.saveOrUpdate(jim);
  17.  
  18. ses.getTransaction().commit();
  19.  
  20. System.out.println("------------------------------------------");
  21. ses = factory.openSession();
  22. ses.beginTransaction();
  23. Department managers_2 = (Department) ses.load(Department.class, 1);
  24.  
  25. // такие изменения не отслеживаются (этот ужасный кусок кода 
  26. // выполняет перестановку местами первого и последнего элемента списка) если я, 
  27. // установил модификатор inverse=true
  28. // managers_2.getEmployies().add(managers_2.getEmployies().remove(0));
  29.  
  30. Employee jim_2 = managers_2.getEmployies().get(1);
  31. // попробуем явно задать порядковый номер сотруднику в отделе 
  32. 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)
А как, интересно, будут выглядеть данные после чтения из hibernate во второй раз?



Как видите, фактически список из дочерних к отделу сотрудников состоит из пяти элементов, просто номеров 1, 3 нет (они равны null). Так что играться подобным образом и задавать порядковый номер сотрудника, чтобы он был использован при чтении коллекции, не стоит: проблем больше чем преимуществ. С другой стороны, когда-то давно я создал свой маленький велосипедик решающий эту проблему, но об этом в другой раз.

Теперь попробуем создать коллекцию вида bag. Физически эта коллекция представляется в классе отдела коллекций List (точно также как и list), вот только никакого автоматического упорядочения при сохранении записей выполняться не будет.
  1. <hibernate-mapping package="experimental.business">
  2.     <class name="Department" dynamic-insert="true" dynamic-update="true">
  3.         <id name="department_id">
  4.             <generator class="native" />
  5.         </id>
  6.         <property name="caption" />
  7.         <bag name="employies" cascade="all" inverse="true" >
  8.             <key column="fk_department_id"  />
  9.             <one-to-many class="Employee"  />
  10.         </bag>
  11.     </class>
  12. </hibernate-mapping>
Последним из тривиальных видов коллекций рассмотрим массив, использование его очень ограничено из-за того, что динамически изменять размер массива не возможно, но мало ли.

Сначала я переписываю код класса Отдел, меняю тип поля employees на Array:
  1. Employee [] employies;
Теперь меняю правила отображения:
  1. <array name="employies" cascade="all" inverse="true" >
  2.    <key column="fk_department_id"  />
  3.    <list-index column="uo" />
  4.    <one-to-many class="Employee"  />
  5. </array>
Как видите, при создании массива и списка мы можем указать дополнительную колонку в подчиненном классе, которая будет хранить порядковый номер элемента.

Еще одна тривиальная и не очень полезная структура данных – массив примитивов. Собственно я ей никогда не пользовался, т.к. с появлением jdk 1.5 проблема появился boxing-outboxing.

Теперь сложное и интересное – карта. Мы можем в состав отдела ввести не просто список сотрудников, но Map. В котором, как роль ключа, так и роль значения могут играть и произвольные (не Domain-объекты), так и замапленные на hibernate классы. Для примера я создам ассоциацию: дата рождения (в виде полноценного java.util.Date) – объект сотрудник.
  1. public class Department {
  2.     Integer department_id;
  3.     String caption;
  4.  
  5.     Map<Date, Employee> employies = new HashMap<Date, Employee>();
  6.  
  7.     public Department() {
  8.     }
  9.  
  10.     public Department(String caption) {
  11.         this.caption = caption;
  12.     }
  13.  
  14.     public Integer getDepartment_id() {
  15.         return department_id;
  16.     }
  17.  
  18.     public void setDepartment_id(Integer department_id) {
  19.         this.department_id = department_id;
  20.     }
  21.  
  22.     public String getCaption() {
  23.         return caption;
  24.     }
  25.  
  26.     public void setCaption(String caption) {
  27.         this.caption = caption;
  28.     }
  29.  
  30.     public Map<Date, Employee> getEmployies() {
  31.         return employies;
  32.     }
  33.  
  34.     public void setEmployies(Map<Date, Employee> employies) {
  35.         this.employies = employies;
  36.     }
  37. }
Теперь внимание на код маппинга: в нем мне снова пришлось избавиться от атрибута inverse=”true”. В противном случае в базу данных в “автоматически-добавленное” поле birthdate не вносились бы значения ключа для map (дата), а только – null.
  1. <hibernate-mapping package="experimental.business">
  2.     <class name="Department" dynamic-insert="true" dynamic-update="true">
  3.         <id name="department_id">
  4.             <generator class="native" />
  5.         </id>
  6.         <property name="caption" />
  7.         <map name="employies" cascade="all"  >
  8.             <key column="fk_department_id"  />
  9.             <map-key type="timestamp" column="birthdate" />
  10.             <one-to-many class="Employee"  />
  11.         </map>
  12.     </class>
  13. </hibernate-mapping>
Вот пример кода:
  1. ses.beginTransaction();
  2.  
  3. Employee jim = new Employee("jim");
  4. Employee tom = new Employee("tom");
  5. Employee ron = new Employee("ron");
  6.  
  7. Department managers = new Department("managers");
  8.  
  9. jim.setDepartment(managers);
  10. tom.setDepartment(managers);
  11. ron.setDepartment(managers);
  12.  
  13. Calendar bd_jim = new GregorianCalendar(2006, 1, 1);
  14. Calendar bd_tom = new GregorianCalendar(2002, 6, 8);
  15. Calendar bd_ron = new GregorianCalendar(2001, 11, 16);
  16.  
  17. managers.getEmployies().put(bd_tom.getTime(), tom);
  18. managers.getEmployies().put(bd_ron.getTime(), ron);
  19. managers.getEmployies().put(bd_jim.getTime(), jim);
  20.  
  21.  
  22. ses.saveOrUpdate(jim);
  23. ses.getTransaction().commit();
  24.  
  25. // ---------------------------
  26.  
  27. ses = factory.openSession();
  28. ses.beginTransaction();
  29. Department managers_2 = (Department) ses.load(Department.class, 1);
  30. Map<Date,Employee> employies = managers_2.getEmployies();
  31. for (Date date : employies.keySet()) {
  32.     Employee emp = employies.get(date);
  33.     System.out.println("date = " + date + " " + emp.getFio());
  34. }
  35. ses.getTransaction().commit();
А вот пример выполнения кода:
  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.   `birthdate` datetime DEFAULT NULL,
  6.   PRIMARY KEY (`employee_id`),
  7.   KEY `FK4AFD4ACE28F13B88` (`fk_department_id`),
  8.   CONSTRAINT `FK4AFD4ACE28F13B88` FOREIGN KEY (`fk_department_id`) REFERENCES `department` (`department_id`)
  9. ) 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)
Чтобы завершить рассмотрение методик работы с Map, давайте попробуем перевенуть нашу карту и в качестве ключа использовать некоторый произвольный объект, например, EmplyeeKey. Он будет состоять из некоторой строки текста, например, код карточки доступа сотрудника. На самом деле, если в качестве ключа вы используете только одну строку, то строить вавилонскую башню с еще одним вспомогательным классом не нужно. Я же делаю так только ради демонстрации. Но сначала задумаемся: а как на уровне БД должна быть реализована наша ассоциация? Когда мы говорили об “индексации” сотрудников объектом дата, то там все было очевидно: в подчиненную таблицу (сотрудник добавлялось еще одно “фиктивное” поле, которое на стадии чтения информации из БД использовалось как ключ для Map). Если в роли ключа находится столь сложный объект, то у нас есть две альтернативы: либо вводить собственный тип данных, инкапсулирующий в себя значение кода карточки сотрудника (тогда все поля ключа будут внедрены в класс сотрудника). Либо создается еще одна таблица, хранящая значения кодов сотрудников, а затем в таблице сотрудники будет добавлено еще одно поле, ссылающееся на идентификатор (целое число) таблицы кодов.

Сначала я создаю новый класс EmployeeKey:
  1. /**
  2.  * Класс карточки сотрудника.
  3.  */
  4. public class EmployeeKey {
  5.     Integer id;
  6.     String code;
  7.  
  8.     public EmployeeKey() {
  9.     }
  10.  
  11.     public EmployeeKey(String code) {
  12.         this.code = code;
  13.     }
  14.  
  15.     public Integer getId() {
  16.         return id;
  17.     }
  18.  
  19.     public void setId(Integer id) {
  20.         this.id = id;
  21.     }
  22.  
  23.     public String getCode() {
  24.         return code;
  25.     }
  26.  
  27.     public void setCode(String code) {
  28.         this.code = code;
  29.     }
  30. }
Затем создаю для него маппинг:
  1. <!DOCTYPE hibernate-mapping PUBLIC
  2.   "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
  3.   "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
  4.  
  5. <hibernate-mapping package="experimental.business">
  6.     <class name="EmployeeKey">
  7.         <id name="id" >
  8.             <generator class="native" />
  9.         </id>
  10.         <property name="code" />
  11.     </class>
  12.  
  13. </hibernate-mapping>
Теперь смотрим, как поменялся код класса Department:
  1. public class Department {
  2.     Integer department_id;
  3.     String caption;
  4.  
  5.     Map<EmployeeKey, Employee> employies = new HashMap<EmployeeKey, Employee>();
  6.  
  7.     public Department() {
  8.     }
  9.  
  10.     public Department(String caption) {
  11.         this.caption = caption;
  12.     }
  13.  
  14.     public Integer getDepartment_id() {
  15.         return department_id;
  16.     }
  17.  
  18.     public void setDepartment_id(Integer department_id) {
  19.         this.department_id = department_id;
  20.     }
  21.  
  22.     public String getCaption() {
  23.         return caption;
  24.     }
  25.  
  26.     public void setCaption(String caption) {
  27.         this.caption = caption;
  28.     }
  29.  
  30.     public Map<EmployeeKey, Employee> getEmployies() {
  31.         return employies;
  32.     }
  33.  
  34.     public void setEmployies(Map<EmployeeKey, Employee> employies) {
  35.         this.employies = employies;
  36.     }
  37. }
И его обновленный файл маппинга:
  1. <hibernate-mapping package="experimental.business">
  2.     <class name="Department" dynamic-insert="true" dynamic-update="true">
  3.         <id name="department_id">
  4.             <generator class="native" />
  5.         </id>
  6.         <property name="caption" />
  7.         <map name="employies" cascade="all"  >
  8.             <key column="fk_department_id"  />
  9.             <map-key-many-to-many class="EmployeeKey" />
  10.             <one-to-many class="Employee"  />
  11.         </map>
  12.     </class>
  13. </hibernate-mapping>
Здесь все очень просто: прежде всего, не забываем убрать атрибут inverse=”true” и меняем тег “map-key” на “map-key-many-to-many”. Несмотря на столь страшное название никакой промежуточной таблицы (обычно используемой для ассоциации многие-ко-многим) генерироваться не будет. Теперь мне нужно указать какой класс будет играть роль ключа (EmployeeKey) и как будет названо поле в таблице Employee.

Теперь пример кода тестирующего измененную структуру данных:
  1. ses.beginTransaction();
  2.  
  3. Employee jim = new Employee("jim");
  4. Employee tom = new Employee("tom");
  5. Employee ron = new Employee("ron");
  6. Department managers = new Department("managers");
  7.  
  8. jim.setDepartment(managers);
  9. tom.setDepartment(managers);
  10. ron.setDepartment(managers);
  11.  
  12. EmployeeKey key_tom = new EmployeeKey("abba-tom");
  13. managers.getEmployies().put(key_tom, tom);
  14. EmployeeKey key_ron = new EmployeeKey("barr-ron");
  15. managers.getEmployies().put(key_ron, ron);
  16. EmployeeKey key_jim = new EmployeeKey("terr-jim");
  17. managers.getEmployies().put(key_jim, jim);
  18.  
  19. ses.saveOrUpdate(jim);
  20.  
  21. // сохраняем все ключи, на них не действует запись cascade="all"
  22. ses.saveOrUpdate(key_jim);
  23. ses.saveOrUpdate(key_ron);
  24. ses.saveOrUpdate(key_tom);
  25.  
  26. ses.getTransaction().commit();
  27.  
  28. System.out.println("------------------------------------------");
  29.  
  30. ses = factory.openSession();
  31. ses.beginTransaction();
  32. Department managers_2 = (Department) ses.load(Department.class, 1);
  33.  
  34. Map<EmployeeKey, Employee> employies = managers_2.getEmployies();
  35. for (EmployeeKey key : employies.keySet()) {
  36.   Employee emp = employies.get(key);
  37.   System.out.println("key = " + key.getCode() + " " + emp.getFio());
  38. }
  39.  
  40. ses.getTransaction().commit();
После запуска приложения будет сгенерирована такая структура данных:



Последнее о чем я сегодня расскажу - это сортировка коллекций при загрузке. В hibernate есть два метода как можно сортировать элементы коллекций (не всех очевидно, так для list сортировка не имеет фактического смысла из-за того, что упорядочение идет по индексу). Однако set и map можно упорядочить (у элемента bag, idbag также нет подобной функциональности).

Количество действий которое нужно выполнить для того чтобы отсортировать коллекцию сотрудников отдела достаточно велико (собственно говоря, я никогда не использовал функцию сортировки при работе с Entity или композитными элемента, только когда использовал коллекцию из примитивов: чисел, строк).

Начнем с того, что есть два варианта упорядочения: на стадии запроса данных из БД и средствами именно БД, и второй вариант (более гибкий) после того как объекты были загружены в память, то они сортируется классическим Collections.sort (только не спрашивайте меня, почему столь простую операцию нужно возложить на hibernate, читай, жестко "забить в конфиге", а не выполнить "ручками").

Начнем с переработки класса Отдел, нужно изменить тип коллекции сотрудников с Set на SortedSet. Теперь подумаем, раз я создал реализацию SortedSet-а в виде TreeSet, то при добавлении в отдел сотрудников (начиная со второго, если быть точным) эти сотрудники начинают сортироваться (интересно как, если наш оригинальный класс не реализовывал интерфейс Comparable). Наиболее простым выходом из ситуации будет создать специальный класс "сортировальщик абы-как" (его код я покажу чуть позже):
  1. public class Department {
  2.     Integer department_id;
  3.     String caption;
  4.  
  5.     SortedSet<Employee> employies = new TreeSet<Employee>(new SortEmployeeAsIs());
  6.  
  7.     public Department() {
  8.     }
  9.  
  10.     public Department(String caption) {
  11.         this.caption = caption;
  12.     }
  13.  
  14.     public Integer getDepartment_id() {
  15.         return department_id;
  16.     }
  17.  
  18.     public void setDepartment_id(Integer department_id) {
  19.         this.department_id = department_id;
  20.     }
  21.  
  22.     public String getCaption() {
  23.         return caption;
  24.     }
  25.  
  26.     public void setCaption(String caption) {
  27.         this.caption = caption;
  28.     }
  29.  
  30.  
  31.     public SortedSet<Employee> getEmployies() {
  32.         return employies;
  33.     }
  34.  
  35.     public void setEmployies(SortedSet<Employee> employies) {
  36.         this.employies = employies;
  37.     }
  38. }
Теперь код "сортировальщика как-есть":
  1. /**
  2.  * Сортируем сотрудников нашего отдела, по их hashCode.
  3.  * Ну и что здесь такого странного, в любом случае это работает только на первом этапе
  4.  * до загрузки отдела из БД
  5.  */
  6. public class SortEmployeeAsIs implements Comparator {
  7.     public int compare(Object o1, Object o2) {
  8.         if (o1 == null) return -1;
  9.         if (o2 == null) return +1;
  10.         return o1.hashCode() - o2.hashCode();
  11.     }
  12. }
Теперь начинаем менять код файла маппинга, там мне нужно декларировать, что содержимое коллекции employee сортируется либо на основании их натурального порядка, либо с помощью явно заданного класса "сравнителя-уже-например-по-фио-сотрудника":
  1. <hibernate-mapping package="experimental.business">
  2.     <class name="Department" dynamic-insert="true" dynamic-update="true">
  3.         <id name="department_id">
  4.             <generator class="native"/>
  5.         </id>
  6.         <property name="caption"/>
  7.         <set name="employies" cascade="all" inverse="true" sort="experimental.business.etc.ComparatorByFIO">
  8.             <key column="fk_department_id"/>
  9.             <one-to-many class="Employee"/>
  10.         </set>
  11.     </class>
  12. </hibernate-mapping>
Как видите я указал полное имя класса experimental.business.etc.ComparatorByFIO, и вот его код:
  1. /**
  2.  * Сравнение Сотрудников выполняется на основании их ФИО
  3.  */
  4. public class ComparatorByFIO implements Comparator {
  5.     public int compare(Object o1, Object o2) {
  6.         Employee u1  = (Employee)o1;
  7.         Employee u2  = (Employee)o2;
  8.         return u1.getFio().compareTo(u2.getFio());
  9.     }
  10. }
Теперь после загрузки отдела, содержимое его коллекции будет упорядочено по fio (не забудьте только вернуть объявление Set-а в классе отдела обратно на обычную "не-сортируемую-версию"):
  1. <set name="employies" cascade="all" inverse="true" order-by="fio">
  2.        <key column="fk_department_id"/>
  3.        <one-to-many class="Employee"/>
  4.   </set>
Аналогичным образом (указав имя поля), можно выполнить сортировку и композитных элементов коллекции.

На этом все.

Categories: Java