Hibernate: каскадные обновления, инверсия отношений и прочая и прочая

Добавлено : 2 Nov 2008, 14:56

Вот пришло время и мне написать пару строчек про hibernate. Я попробую сделать небольшой cheatsheet по вопросу двусторонней ассоциации, каскадных обновлений, ленивой загрузки и прочего и прочего. Сразу предупрежу, что я довольно негативно отношусь к hibernate, предпочитаю в практике использовать ibatis. Может, причиной является мой опыт в проектировании БД, и я всегда предпочитаю идти именно от базы к модели классов java, а не наоборот. Большинство проблем, которые возникают у новичков заключается в том, что они забывают, что база данных живет по другим правилам, чем слой объектов. В СУБД нет всех этих двусторонних связей, да и в понятие каскадных обновлений вкладывается немного другой смысл. Естественно, что я не исключаю ситуации, “что фокусник был пьян и фокус не удался”, так что ваши замечания будут для меня полезны. Несмотря на то, что я излагаю пример на базе mysql, я полагаю, что основные идеи и выводы будут применимы для любой СУБД. Одним словом, поехали:

Для примера есть такая модель данных из двух таблиц: Сотрудники и Отделы. Отношения между ними в терминах СУБД “один-ко-многим”, где на стороне “один” находится Отдел, а на стороне “много” находится “Сотрудники”. В каждую из табличек я добавлю первичный ключ - id, название отдела/ФИО сотрудника, остальные поля не существенны. Сразу же нужно определиться с тем, будут ли наши сотрудники существовать вне отделов (и это решение очень, очень важное). Предположим, что такое не возможно: сотрудник без отдела тут же удаляется, обратная же ситуация вполне возможна: отдел может существовать без сотрудников сколь угодно долго. Для реализации связи между этим двумя таблицами я должен в таблицу “сотрудники” добавить внешний ключ “fk_department_id”. Надо сказать, что когда я создаю внешний ключ с помощью sql, то могу/скорее должен указать модификаторы этого внешнего ключа. Например, для mysql, этот код будет выглядеть так:

Изображение:hiber_1_1.png

Так я создаю таблицу “отделы”:

CREATE TABLE `department` (
 `department_id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
 `caption` varchar(255) DEFAULT NULL) ENGINE=InnoDB;

Обратите внимание, что на уровне базы нет никаких упоминаний, что есть связь между отделом и сотрудниками, и это очень логично, ведь именно сотрудники заинтересованы в том, чтобы быть привязанными к отделам. Всегда зависимая, подчиненная сторона ссылается на главную таблицу. Теперь код создающий таблицу “сотрудники”:

CREATE TABLE `employee` (
 `employee_id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
 `fio` varchar(255) DEFAULT NULL,
 `fk_department_id` int(11) NOT NULL,
 FOREIGN KEY (`fk_department_id`) REFERENCES `department` (`department_id`) БЛА-БЛА-БЛА
) ENGINE=InnoDB ;

Теперь давайте разберемся с приведенным выше кодом. В целом все просто: две таблицы, в каждой первичный ключ, тестовое поле. В таблицу employee добавлено поле, типа целое число, для этого поля запрещено вводить значение поля равное NULL (т.е. вот оно то ограничение, о котором я говорил, что сотрудников без отдела быть не может, любая попытка очистить поле “номер отдела будет блокироваться на уровне базы данных”). В случае, если правка выполняется в подчиненной таблице (сотрудники), то при выполнении операции вставки или модификации записи mysql проверят то, чтобы в главной таблице была запись с таким номером отдела, на который я пытаюсь сослаться при вставке подчиненно записи. в следующем примере я нарушил это правило, попытавшись добавить сотрудника “Тома” в отдел номер 2 и это вызвало ошибку. Важно, что mysql не делает никаких предположений на предмет того, что “ну и что что отдела номер два нет, я может через секунду его добавлю, честное-честное”. Порядок внесения изменений должен быть таким: сначала добавим запись в главную таблицу и только затем в подчиненную. Когда мы будем писать hibernate код, то это ограничение будет для нас важным.

drop table if exists employee;
drop table if exists departments;
 
CREATE TABLE `department` (
 `department_id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
 `caption` varchar(255) DEFAULT NULL) ENGINE=InnoDB;
 
 
CREATE TABLE `employee` (
 `employee_id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
 `fio` varchar(255) DEFAULT NULL,
 `fk_department_id` int(11) NOT NULL,
 FOREIGN KEY (`fk_department_id`) REFERENCES `department` (`department_id`)
) ENGINE=InnoDB ;
 
 
mysql> insert into department values (NULL, 'managers');
Query OK, 1 row affected (0.03 sec)
 
mysql> select * from department;
+---------------+----------+
| department_id | caption  |
+---------------+----------+
|             1 | managers |
+---------------+----------+
1 row in set (0.02 sec)
 
mysql> insert into employee values (NULL, 'Jim', 1);
Query OK, 1 row affected (0.02 sec)
 
mysql> select * from employee;
+-------------+------+------------------+
| employee_id | fio  | fk_department_id |
+-------------+------+------------------+
|           1 | Jim  |                1 |
+-------------+------+------------------+
1 row in set (0.00 sec)
 
mysql> insert into employee values (NULL, 'Tom', 2);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`testmach/employee`, CONSTRAINT `e
mployee_ibfk_1` FOREIGN KEY (`fk_department_id`) REFERENCES `department` (`department_id`))

Так, а что такое “БЛА-БЛА-БЛА”? А это модификаторы, управляющие тем, как mysql будет выполнять операции связанные с модификацией записей в главной таблице. Ведь после правки главной записи, мы не должны допустить того, что в подчиненной таблице возникли потерянные ссылки, указывающие на устаревшую информацию. В mysql есть следующие модификаторы:

CASCADE. Каскадные обновления. В коде sql записываются так:

FOREIGN KEY (`fk_department_id`) REFERENCES `department` (`department_id`) ON DELETE cascade ON UPDATE cascade

Значит, любая правка в главной таблице будет приводить к немедленному изменению в подчиненной таблице. Так удаление главной записи будет приводить к автоматическому удалению всех сотрудников отдела. Изменение номера отдела (например, с 2 на 3) приведет к тому, что во все карточки сотрудников также будет внесена правка и значение поля fk_department_id станет равным 3.

SET NULL. При изменении главной записи в подчиненной таблице значение поля fk_department_id для всех затронутых изменением отдела сотрудников станет равным null. Соотвественно, такой режим возможен лишь тогда, когда поле fk_department_id было объявлено с модификатором NULL. Явно не наш случай.

NO ACTION. Запрет на выполнение операции. Фактически если я хочу удалить или перенумеровать отдел, то выполнить это не возможно до тех пор, пока у отдела есть сотрудники. Что же довольно логично: перед расформированием отдела нужно предварительно разобраться с его сотрудниками, например, удалить или перевести в другой отдел.

Такое же поведение, как и NO ACTION вызывает RESTRICT. Точно такое же поведение происходит и тогда, когда я не указываю явно какой-либо из модификаторов.

Последний модификатор: SET DEFAULT. Фактического значения не имеет т.к. mysql игнорирует данное выражение.

Теперь приведем код java классов: отдел и сотрудник.

package experimental.business;

import java.util.Set;

/**
* Отдел
*/
public class Department {
 
Integer department_id;
  String caption;
  Set<Employee> employies =
new HashSet<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 Set<Employee> getEmployies() {
   
return employies;
 
}

 
public void setEmployies(Set<Employee> employies) {
   
this.employies = employies;
 
}
}

А теперь сотрудник:

package experimental.business;

/**
* Сотрудник
*/
public class Employee {
 
Integer employee_id;
  String fio;
  Department department;

 
public Employee() {
  }

 
public Employee(String fio) {
   
this.fio = fio;
 
}

 
public Integer getEmployee_id() {
   
return employee_id;
 
}

 
public void setEmployee_id(Integer employee_id) {
   
this.employee_id = employee_id;
 
}

 
public String getFio() {
   
return fio;
 
}

 
public void setFio(String fio) {
   
this.fio = fio;
 
}

 
public Department getDepartment() {
   
return department;
 
}

 
public void setDepartment(Department department) {
   
this.department = department;
 
}
}

Как видите ничего сложного: в классе сотрудника есть поле Department играющее роль ссылки на отдел в котором трудится наш герой, в таблице Employee есть ссылка на список (set) сотрудников зачисленных в отдел.

Теперь переходим к написанию правил отображений “реляция-классы-и-обратно”:

Сначала отображение для отдела:

<!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="Department">
        <id name="department_id">
            <generator class="native" />
        </id>
        <property name="caption" />
        <set name="employies">
            <!-- для организации связи между таблицами, нужно поместить 
            в класс зависящий от главной таблицы внешний ключ -->
            <key column="fk_department_id" />
            <one-to-many class="Employee" />
        </set>
    </class>
</hibernate-mapping>

Теперь отображение для сотрудника:

<!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="Employee">
        <id name="employee_id">
            <generator class="native"/>
        </id>
        <property name="fio"/>
        <!--
        Здесь также нужно указать, что связь между сотрудником и отделом обспечивается за счет
        поля fk_department_id помещаемого именно в эту таблицу (сотрудника)
        Ну а модификатор  not-null="true" говорит, что сотрудники не могут существовать вне отдела
        -->
        <many-to-one name="department" class="Department" column="fk_department_id" not-null="true"/>
    </class>
</hibernate-mapping>

Обращайте внимание, что в теге many-to-one для сотрудника, я указал имя колонки fk_department_id, которая должна быть добавлена в эту таблицу (сотрудника) чтобы иметь возможность связаться с главной таблицей, отделом. Аналогично в таблице отделов я задекларировал, что <key column="fk_department_id" />. Имена этих полей должны обязательно совпадать: ведь это одно и тоже поле. К сожалению, hibernate не слишком строг, чтобы ткнуть вас носом: мол, если эти поля названы по разному, то это приведет к неразберихе и ошибке. Поэтому я крайне негативно отношусь к тем, кто не указывает явно значения этих атрибутов, ведь в таком случае имя служебной колонки будет вычисляться самим hibernate и по-умолчанию будет равно имени поля в java-классе. Для примера я убрал эти явные имена, сгенерировал структуру БД и получил вот такое:

CREATE TABLE `employee` (
  `employee_id` int(11) NOT NULL AUTO_INCREMENT,
  `fio` varchar(255) DEFAULT NULL,
  `department` int(11) DEFAULT NULL,
  `id` int(11) DEFAULT NULL,
  PRIMARY KEY (`employee_id`),
  KEY `FK4AFD4ACEE21047AC` (`department`),
  KEY `FK4AFD4ACEAF821175` (`id`),
  CONSTRAINT `FK4AFD4ACEAF821175` FOREIGN KEY (`id`) REFERENCES `department` (`department_id`),
  CONSTRAINT `FK4AFD4ACEE21047AC` FOREIGN KEY (`department`) REFERENCES `department` (`department_id`)
) ENGINE=InnoDB;

Как внешние ключи были объявлены сразу две колонки: id и department и это не хорошо. Правильный вариант должен быть таким:

CREATE TABLE `employee` (
 `employee_id` int(11) NOT NULL AUTO_INCREMENT,
 `fio` varchar(255) DEFAULT NULL,
 `fk_department_id` int(11) DEFAULT NOT NULL,
 PRIMARY KEY (`employee_id`),

 KEY `FK4AFD4ACE28F13B88` (`fk_department_id`),
 CONSTRAINT `FK4AFD4ACE28F13B88` FOREIGN KEY (`fk_department_id`) REFERENCES `department` (`department_id`)
 ENGINE=InnoDB;

И тут самое время задуматься: а где здесь модификаторы ON DELETE CASCADE или ON DELETE SET NULL – ничего нет. Может быть, нужно написать какое-то ключевое слово где-нибудь в конфигурационном файле? Не нужно, да и нет такого секретного места. Не забывайте что hibernate это инструмент универсальный и умеющий работать не только с mysql, но postgres, mssql … И подобные on delete … могут быть не поддерживаемыми и не иметь какого-то встроенного аналога для какой-то СУБД. Тот же mysql поддерживает внешние ключи только для движка innodb, а Microsoft sql server 7.0 (это было еще до 2000), каскадные операции также делать не умел, и приходилось их делать с помощью триггеров (вот такие были темные времена).

Так значит, что мы должны будем заботиться об удалении и изменении подчиненных записей сами? Нет не должны: hibernate сделает это и многое другое за нас. Надо только правильно настроить каскадные операции (и как вы уже поняли, что эти каскадные операции с каскадами в СУБД не имеют ничего общего). Но сначала пример:

public static void main(String[] args) {
 
Configuration configuration = new Configuration().configure();
  SessionFactory factory = configuration.buildSessionFactory
();
  Session ses = factory.openSession
();

  ses.beginTransaction
();

  Employee jim =
new Employee("jim");
  Department managers =
new Department("managers");
  managers.getEmployies
().add(jim);
  jim.setDepartment
(managers);

  ses.saveOrUpdate
(managers);
  ses.saveOrUpdate
(jim);

  ses.getTransaction
().commit();
}

По крайней мере, именно, так рекомендуют поступать в разных книжках: создали два объекта – сотрудник и отдел, привязали их друг к другу (два раза: и отдел к сотруднику и сотрудника к отделу). После сохранения проверили таблицы в СУБД: да новые записи появились и выглядят просто прекрасно.

mysql> select * from employee;
+-------------+------+------------------+
| employee_id | fio  | fk_department_id |
+-------------+------+------------------+
|           1 | jim  |                1 |
+-------------+------+------------------+
1 row in set (0.00 sec)

mysql> select * from department;
+---------------+----------+
| department_id | caption  |
+---------------+----------+
|             1 | managers |
+---------------+----------+
1 row in set (0.00 sec)

Как видите, значение поля fk_department_id в таблице сотрудников равно 1 (отдел менеджеров).

Теперь попробуем другой сценарий: все как раньше но при сохранении я укажу другой порядок сохранения объектов: сначала сотрудника, затем отдел. Тут я надеюсь, что будет ошибка: ведь на уровне базы стоит запрет, что нельзя сохранить сначала сотрудника (ссылающегося на еще не существующий отдел), и только затем сам отдел:

ses.saveOrUpdate(jim);
ses.saveOrUpdate
(managers);

Ух-ты получилось:

Exception in thread "main" org.hibernate.PropertyValueException:
not-
null property references a null or transient value: experimental.business.Employee.department

Правда, первая часть ошибки не про нас: ведь значение поля department у нашего Джима ну никак не может быть равным null, а вот что значит слово transient? А значит, что при сохранении Джима, hibernate не смог этого выполнить т.к. объект department еще не был сохранен – все как и ожидалось. Хоть и не приятно, но понятно. Теперь такой эксперимент:

ses.saveOrUpdate(managers);
//ses.saveOrUpdate(jim);

Тоже ошибка:

Exception in thread "main" org.hibernate.TransientObjectException: object references an unsaved transient
 
instance - save the transient instance before flushing: experimental.business.Employee

А вот это странно: как это hibernate не смог сохранить отдел из-за того, что в нем есть сотрудник которого мы не сохранили. Ну ладно, неприятно, но ведь можно запомнить, что сохранять объекты нужно всегда парами и всегда в нужно порядке. Считаем, что как-будто проблему мы решили и смело идем дальше.

А как насчет удаления отдела или сотрудника?

После завершения первой транзакции я снова начинаю ее, загружаю отдел под номером 1 и пытаюсь удалить его:

ses.beginTransaction();
Department managers_2 =
(Department) ses.load(Department.class, 1);
ses.delete
(managers_2);
ses.getTransaction
().commit();

И снова получаю ошибку:

Exception in thread "main" org.hibernate.exception.ConstraintViolationException:
  Could not execute JDBC batch update
  …… пропущено без потери смысла ….
Caused by: java.sql.BatchUpdateException: Column
"fk_department_id" cannot be null

Да вспоминаю, действительно я поставил ограничение на то, что значение поля fk_department_id не может быть null, но причем здесь именно такая ошибка? Ведь я хотел удалить отдел … Стоп, от отдела зависит сотрудник. Нельзя удалить отдел, не сделав что-то предварительное с сотрудником, ведь иначе будет нарушена целостность СУБД (те самые внешние ключи, про которые я рассказывал в начале статьи). А теперь главный вопрос: как вы думаете, что должен сделать hibernate с сотрудником, когда его отдел удаляется? Похоже, он решил, что нужно отчислить сотрудника из отдела, но с работы не увольнять. Отлично, давайте немного поиграемся и сделаем два варианта: перед удалением отдела мы переведем всех его сотрудников из одного отдела в другой, а во второй раз, удалим сотрудников перед уничтожением отдела:

В следующем примере я для полной красоты добавил два отдела (дизайнеры и менеджеры) и трех человек персонала, первоначально зачисленных в отдел менеджмента, затем они должны быть переведены к дизайнерам:

public static void main(String[] args) {
 
Configuration configuration = new Configuration().configure();
  SessionFactory factory = configuration.buildSessionFactory
();
  Session ses = factory.openSession
();

  ses.beginTransaction
();

  Employee jim =
new Employee("jim");
  Employee tom =
new Employee("tom");
  Employee ron =
new Employee("ron");
  Department managers =
new Department("managers");
  Department designers =
new Department("designers");
  managers.getEmployies
().add(jim);
  managers.getEmployies
().add(tom);
  managers.getEmployies
().add(ron);

  jim.setDepartment
(managers);
  tom.setDepartment
(managers);
  ron.setDepartment
(managers);

  ses.saveOrUpdate
(managers);
  ses.saveOrUpdate
(designers);
  ses.saveOrUpdate
(jim);
  ses.saveOrUpdate
(tom);
  ses.saveOrUpdate
(ron);

  ses.getTransaction
().commit();

  System.out.println
("------------------------------------------");
 
// ---------------------------
 
ses.beginTransaction();

  Department managers_2 =
(Department) ses.load(Department.class, 1);
  Department designers_2 =
(Department) ses.load(Department.class, 2);
  Employee
[] employies = managers_2.getEmployies().toArray(
     
new Employee[] {});

 
for (Employee employy : employies) {
   
managers_2.getEmployies().remove(employy);
    designers_2.getEmployies
().add(employy);
    employy.setDepartment
(designers_2);
 
}
 
ses.delete(managers_2);
  ses.getTransaction
().commit();
}

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

ses.beginTransaction();
Department managers_2 =
(Department) ses.load(Department.class, 1);
Department designers_2 =
(Department) ses.load(Department.class, 2);
Employee
[] employies = managers_2.getEmployies().toArray(new Employee[]{});

for (Employee employy : employies) {
 
managers_2.getEmployies().remove(employy);
  employy.setDepartment
(null);
  ses.delete
(employy);
}

ses.delete(managers_2);

ses.getTransaction
().commit();

А-а-а. не получилось. Снова это гадкое исключение:

Caused by: java.sql.BatchUpdateException: Column "fk_department_i" cannot be null

Ну, как же мне от тебя избавиться?!

Удалить отдел вместе с сотрудниками я не смог, только переместить в другой отдел. Более того, непонятно зачем столько городить проблем с этим hibernate, если только на старом добром sql перемещение сотрудников из одного отдела в другой занимало одну строчку кода. А может быть все не так уж и просто? И hibernate может взять на себя эти рутинные действия? В одной замечательной книжке есть следующая фраза:

Hibernate supports ten different types of cascades that can be applied to many-to-one associations as well as collections.
The
default cascade is none. Each cascade strategy specifies the operation or operations that should be propagated
to child entities.

Ну, по поводу десяти возможных вариантов каскадных операций это они отстали от жизни (правда не намного, в моей версии hibernate (3.2) операций 13, вместе с none). Но в целом, да, действительно я могу пометить некоторую ассоциацию как cascade, и операции над родительским объектом будут распространяться и на связанную сущность. Пробуем:

В файле department.hbm.xml я заменил декларацию set-а на следующую:

<set name="employies" cascade="all">
   <!-- для организации связи между таблицами, нужно поместить 
    в класс зависящий от главной таблицы внешний ключ -->
   <key column="fk_department_id"  />
   <one-to-many class="Employee"  />
</set>
 
А в файл employee.hbm.xml были внесены такие правки:
 
<source lang="xml">
<many-to-one name="department" class="Department" column="fk_department_id" not-null="true" cascade="all"/>

Запись cascade=”all” означает, что при сохранении, обновлении и удалении родительского объекта изменения будут распространены и на дочерний объект. Каскадные операции установлены у меня с обоих сторон, поэтому для сохранения графа объектов достаточно будет сохранить хотя бы одну сущность:

Session ses = factory.openSession();
ses.beginTransaction
();

Employee jim =
new Employee("jim");
Employee tom =
new Employee("tom");
Employee ron =
new Employee("ron");

Department managers =
new Department("managers");
managers.getEmployies
().add(jim);
managers.getEmployies
().add(tom);
managers.getEmployies
().add(ron);

jim.setDepartment
(managers);
tom.setDepartment
(managers);
ron.setDepartment
(managers);

ses.saveOrUpdate
(jim);

ses.getTransaction
().commit();

В примере я сохранаю одного Джима, но раз он включен в отдел (и есть модификатор связи cascade=all), то будет и сохранен отдел менеджеров, а раз внутри отдела менеджеров есть некоторый набор сотрудников (tom, ron), то будут сохранены и они. Если пометка cascade=all будет одна, то поведение меняется:

Поведение, которое будет, если убрать каскад от сотрудника к отделу.

ses.saveOrUpdate(managers);// а так я сохраню отдел и всех его сотрудников
//ses.saveOrUpdate(jim); // это вызывает ошибку, ведь указания того,
// что отделы привязанные к сотруднику также нужно сохранить нет

И поведение, когда наоборот, убран каскад от отдела к сотруднику.

Employee jim = new Employee("jim");
Employee tom =
new Employee("tom");
Employee ron =
new Employee("ron");
Department managers =
new Department("managers");
managers.getEmployies
().add(jim);

jim.setDepartment
(managers);
tom.setDepartment
(managers);
ron.setDepartment
(managers);

// а вот привязку к отделу других сотрудников делать нельзя т.к. каскадного сохранения от отдела к ним - нет
//managers.getEmployies().add(tom);
//managers.getEmployies().add(ron);

ses.saveOrUpdate(jim); // сохранение сотрудника заставляет сохранить и его отдел

ses.getTransaction().commit();

Теперь перейдем от сохранения объекта к его удалению и посмотрим нету ли там подводных камней (для полного удобства я включил каскадные отношения на обоих сторонах ассоциации):

ses.beginTransaction();
Department managers_2 =
(Department) ses.load(Department.class, 1);
ses.delete
(managers_2);
ses.getTransaction
().commit();

Запускаем и снова получаем исключение:

Caused by: java.sql.BatchUpdateException: Column "fk_department_id" cannot be null

Ну никак у нас не получается начать работать с hibernate и отойти в сторону от sql. Пора начать разбираться какую последовательность запросов посылает на сервер hibernate и в каком месте он решил выполнить установку значения поля 'fk_department_id' на null.

Для этого я в корень своего classpath поместил файли log4j.properties (не забудьте подключить и библиотеку log4j). В нем я включаю журналирование:

### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
 
### direct messages to file hibernate.log ###
#log4j.appender.file=org.apache.log4j.FileAppender
#log4j.appender.file.File=hibernate.log
#log4j.appender.file.layout=org.apache.log4j.PatternLayout
#log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
 
### set log levels - for more verbose logging change 'info' to 'debug' ###
 
log4j.rootLogger=warn, stdout
 
log4j.logger.javax.naming=none
 
#log4j.logger.org.hibernate=info
log4j.logger.org.hibernate=warn
 
### log HQL query parser activity
#log4j.logger.org.hibernate.hql.ast.AST=debug
 
### log just the SQL
log4j.logger.org.hibernate.SQL=debug
 
### log JDBC bind parameters ###
log4j.logger.org.hibernate.type=debug
 
### log schema export/update ###
#log4j.logger.org.hibernate.tool.hbm2ddl=debug
 
### log HQL parse trees
#log4j.logger.org.hibernate.hql=debug
 
### log cache activity ###
#log4j.logger.org.hibernate.cache=debug
 
### log transaction activity
#log4j.logger.org.hibernate.transaction=debug
 
### log JDBC resource acquisition
#log4j.logger.org.hibernate.jdbc=debug
 
### enable the following line if you want to track down connection ###
### leakages when using DriverManagerConnectionProvider ###
#log4j.logger.org.hibernate.connection.DriverManagerConnectionProvider=trace

Запускаем приложение еще раз и наблюдаем странную картину:

12:20:10,250 DEBUG SQL:401 - insert into Department (caption) values (?)
12:20:10,265 DEBUG StringType:133 - binding 'managers' to parameter: 1
12:20:10,265 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
12:20:10,265 DEBUG StringType:133 - binding 'jim' to parameter: 1
12:20:10,281 DEBUG IntegerType:133 - binding '1' to parameter: 2
12:20:10,281 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
12:20:10,281 DEBUG StringType:133 - binding 'ron' to parameter: 1
12:20:10,281 DEBUG IntegerType:133 - binding '1' to parameter: 2
12:20:10,281 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
12:20:10,281 DEBUG StringType:133 - binding 'tom' to parameter: 1
12:20:10,281 DEBUG IntegerType:133 - binding '1' to parameter: 2
12:20:10,281 DEBUG SQL:401 - insert into Department (caption) values (?)
12:20:10,281 DEBUG StringType:133 - binding 'designers' to parameter: 1
12:20:10,296 DEBUG SQL:401 - update Employee set fk_department_id=? where employee_id=?
12:20:10,296 DEBUG IntegerType:133 - binding '1' to parameter: 1
12:20:10,296 DEBUG IntegerType:133 - binding '1' to parameter: 2
12:20:10,296 DEBUG SQL:401 - update Employee set fk_department_id=? where employee_id=?
12:20:10,312 DEBUG IntegerType:133 - binding '1' to parameter: 1
12:20:10,312 DEBUG IntegerType:133 - binding '2' to parameter: 2
12:20:10,312 DEBUG SQL:401 - update Employee set fk_department_id=? where employee_id=?
12:20:10,312 DEBUG IntegerType:133 - binding '1' to parameter: 1
12:20:10,312 DEBUG IntegerType:133 - binding '3' to parameter: 2
------------------------------------------
12:20:10,343 DEBUG SQL:401 - update Employee set fk_department_id=null where fk_department_id=?
12:20:10,343 DEBUG IntegerType:133 - binding '1' to parameter: 1
12:20:10,343  WARN JDBCExceptionReporter:77 - SQL Error: 1048, SQLState: 23000
12:20:10,343 ERROR JDBCExceptionReporter:78 - Column 'fk_department_id' cannot be null

Линией я разделил код который вставляет записи и который пытается их удалить. С самого первой строки начинают расти подозрения, что что-то не так, вот например операция сохранения отделов и сотрудников:

ses.saveOrUpdate(managers);
ses.saveOrUpdate
(jim); // сохранение сотрудника заставляет сохранить и его отдел
ses.saveOrUpdate(designers);

Вызвала следующие шаги:

//Добавляем новый отдел, пока все путем
12:20:10,250 DEBUG SQL:401 - insert into Department (caption) values (?)
12:20:10,265 DEBUG StringType:133 - binding 'managers' to parameter: 1
// в отделе мы узнали, что в нем есть сотрудники, отлично добавляем сотрудника Джима
12:20:10,265 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
12:20:10,265 DEBUG StringType:133 - binding 'jim' to parameter: 1
12:20:10,281 DEBUG IntegerType:133 - binding '1' to parameter: 2
// обратите внимание на предыдущую строку, второй параметр fk_department_id 
// уже знает, что Джима нужно поместить в отдел номер 1.
// далее тривиально сохраняем Рона и Тома (также в отдел номер 1)
12:20:10,281 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
12:20:10,281 DEBUG StringType:133 - binding 'ron' to parameter: 1
12:20:10,281 DEBUG IntegerType:133 - binding '1' to parameter: 2
12:20:10,281 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
12:20:10,281 DEBUG StringType:133 - binding 'tom' to parameter: 1
12:20:10,281 DEBUG IntegerType:133 - binding '1' to parameter: 2
// добавляем еще один отдел дизайнеров, он особой роли не играет
12:20:10,281 DEBUG SQL:401 - insert into Department (caption) values (?)
12:20:10,281 DEBUG StringType:133 - binding 'designers' to parameter: 1
// А это еще что такое? Зачем нужно обновлять сотрудника Джима, устанавливая
// ему значение поля fk_department_id равным 1, 
// ведь Джим уже в составе отдела номер 1.
12:20:10,296 DEBUG SQL:401 - update Employee set fk_department_id=? where employee_id=?
12:20:10,296 DEBUG IntegerType:133 - binding '1' to parameter: 1
12:20:10,296 DEBUG IntegerType:133 - binding '1' to parameter: 2

Операция удаления также очень странная: первым шагом идет обновление таблицы сотрудников, а не удаление из нее Джима (как на то мог бы я надеяться, поставив cascade=all).

------------------------------------------
12:20:10,343 DEBUG SQL:401 - update Employee set fk_department_id=null 
where fk_department_id=?
12:20:10,343 DEBUG IntegerType:133 - binding '1' to parameter: 1

Может быть, нужно удалить Джима явно, перед удалением отдела?

ses.beginTransaction();

Department managers_2 =
(Department) ses.load(Department.class, 1);
Employee jim_2=
(Employee) ses.load(Employee.class, 1);
ses.delete
(jim_2);
ses.delete
(managers_2);

ses.getTransaction
().commit();

Нет, снова то же самое исключение во время ненужного обновления таблицы сотрудников. Ах да, я ведь забыл, что у нас есть связи между Джимом и отделом, давайте избавимся от них.

ses.beginTransaction();

Department managers_2 =
(Department) ses.load(Department.class, 1);
Employee jim_2=
(Employee) ses.load(Employee.class, 1);
managers_2.getEmployies
().remove(jim_2);
jim_2.setDepartment
(null);
ses.delete
(managers_2);

ses.getTransaction
().commit();

Запустили, получили исключение, правда, уже другое:

Exception in thread "main" org.hibernate.PropertyValueException: not-null property references a null
 
or transient value: experimental.business.Employee.department

Ну, это еще понятно: ведь я пытаюсь установить значение поля department для Джима равным null. Надо сказать, что созданное в файле маппинга ограничение проверяется при любой операции синхронизации сессии с базой данных, и если значение поля depertment равно null, то операция отменяется, несмотря на то, что возможно на Джима есть ссылка из отдела – это не имеет значения. Собственно говоря, такое поведение ожидаемое и понятно. Вот только зачем hibernate пытается выполнить это действие, ведь через секунду нашего Джима вместе с отделом не станет. Может выполнить явно удаление Джима?

Department managers_2 = (Department) ses.load(Department.class, 1);
Employee jim_2=
(Employee) ses.load(Employee.class, 1);
managers_2.getEmployies
().remove(jim_2);
jim_2.setDepartment
(null);
ses.delete
(jim_2);
ses.delete
(managers_2);

Нет, снова ошибка:

Caused by: java.sql.BatchUpdateException: Column "fk_department_id" cannot be null

Собственно, все эти проблемы из-за того, что при создании файла маппинга, я задал ограничение на значение поля fk_department_id, чтобы оно было не равно null. Если я уберу этот запрет, то все примеры заработают, пусть не идеально, пусть будет выполняться обновление таблицы сотрудников с установкой значения поля fk_department_id в null, чтобы через секунду удалить этого сотрудника, но все будет работать. И если бы я мог спроектировать с нуля приложение и базу для него, то так бы и сделал, плюнул на ограничения not-null, надеясь, что с базой будут работать только через мою программу и никак иначе. Но нельзя, особенно, если база есть, она была унаследована, есть зоопарк софта работающий с данными и ограничения на уровне базы данных – это последний бастион защиты, и ломать его в угоду тому, что новому “hibernate-танку” неудобно выезжать через ворота – глупо.

Как вывод: нам нужно подстроить hibernate под правила Б.Д. Начнем с того, что поймем, что в базах данных нет двусторонних связей, таких как я спроектировал ранее. Связь реализуется от подчиненного к главному и точка. Например, когда я выполняю типовое назначение сотрудника в отдел, то делаю так:

managers.getEmployies().add(jim);
jim.setDepartment
(managers);

То hibernate считает, что у нас есть две связи, а база данных считает, что связь одна. Именно из-за этого различия в логах sql-команд посылаемых на сервер присутствуют дубляжи: сначала вставим сотрудника в отдел X, затем обновим значение номера отдела (уже правильное) на его же самого.

14:10:32,453 DEBUG SQL:401 - insert into Department (caption) values (?)
--- и через много-много строк --- 
14:10:32,484 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
14:10:32,515 DEBUG SQL:401 - update Employee set fk_department_id=? where employee_id=?

Мы должны сообщить hibernate-у, что эти две связи есть единое целое и одна сторона является отражением другой. Для этого используют модификатор inverse. Им помечается ассоциация находящаяся на строне Один. Если у нас связь не один ко многим, а многие ко многим, то помечать можно любую из сторон, но об этом я расскажу подрбнее попозже. Итак, новый вид файла маппинга для отдела будет выглядеть так:

<set name="employies" cascade="all" inverse="true">
    <!-- для организации связи между таблицами, нужно поместить 
     в класс зависящий от главной таблицы внешний ключ -->
    <key column="fk_department_id"  />
    <one-to-many class="Employee"  />
</set>

Пробуем, сначала сохранение:

ses.saveOrUpdate(managers);
ses.saveOrUpdate
(jim);

Или так, от перестановки слагаемых сумма не изменилась (ведь у сотрудника, равно как и у отдела стоит пометка cascade=”all” и связанные объекты будут сохранены).

ses.saveOrUpdate(jim);
ses.saveOrUpdate
(managers);

Более того, пропали дублирующиеся sql-запросы, теперь журнал выглядит следующим образом:

14:18:02,406 DEBUG SQL:401 - insert into Department (caption) values (?)
14:18:02,437 DEBUG StringType:133 - binding 'managers' to parameter: 1
14:18:02,437 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
14:18:02,437 DEBUG StringType:133 - binding 'tom' to parameter: 1
14:18:02,437 DEBUG IntegerType:133 - binding '1' to parameter: 2
14:18:02,437 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
14:18:02,453 DEBUG StringType:133 - binding 'ron' to parameter: 1
14:18:02,453 DEBUG IntegerType:133 - binding '1' to parameter: 2
14:18:02,453 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
14:18:02,453 DEBUG StringType:133 - binding 'jim' to parameter: 1
14:18:02,453 DEBUG IntegerType:133 - binding '1' to parameter: 2
14:18:02,453 DEBUG SQL:401 - insert into Department (caption) values (?)
14:18:02,453 DEBUG StringType:133 - binding 'designers' to parameter: 1

Однако перед тем как мы восхитимся и перейдем к рассмотрению операции удаления, давайте сделаем еще один эксперимент:

ses.beginTransaction();

Employee jim =
new Employee("jim");
Employee tom =
new Employee("tom");
Employee ron =
new Employee("ron");
Department managers =
new Department("managers");

// я не могу не назначить сотруднику отдел т.к. у меня стоит ограничение not-null
// и оно проверяется еще до отправки данных в СУБД
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();

Смотрим журнал сообщений:

14:25:45,656 DEBUG SQL:401 - insert into Department (caption) values (?)
14:25:45,671 DEBUG StringType:133 - binding 'managers' to parameter: 1
14:25:45,687 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
14:25:45,687 DEBUG StringType:133 - binding 'tom' to parameter: 1
14:25:45,687 DEBUG IntegerType:133 - binding '1' to parameter: 2
14:25:45,687 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
14:25:45,703 DEBUG StringType:133 - binding 'ron' to parameter: 1
14:25:45,703 DEBUG IntegerType:133 - binding '1' to parameter: 2
14:25:45,703 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
14:25:45,703 DEBUG StringType:133 - binding 'jim' to parameter: 1
14:25:45,703 DEBUG IntegerType:133 - binding '1' to parameter: 2

Ага, когда я сказал сохранить Джима, то hibernate воспользовался ассоциацией, чтобы найти и сохранить отдел Джима (первое действие). Затем он начинает, также пользуясь установленным атрибутом cascade=”all”, добавлять сотрудников (сначала Том, затем Рон - точно в таком же порядке записи и добавлялись в set отдела). Последний шаг – сохранение Джима (в примере от него есть связь к отделу, но от отдела связи к Джиму нет). Теперь еще немного путаницы:

Employee jim = new Employee("jim");
Employee tom =
new Employee("tom");
Employee ron =
new Employee("ron");

Department managers =
new Department("managers");
Department designers =
new Department("designers");

jim.setDepartment
(managers);
tom.setDepartment
(managers);
ron.setDepartment
(designers);

managers.getEmployies
().add(tom);
managers.getEmployies
().add(ron);

ses.saveOrUpdate
(jim);

Обратите внимание: в отдел менеджеров через set был добавлен ron, но у него самого связь идет на отдел дизайнеров. Смотрим журнал запросов:

14:31:59,359 DEBUG SQL:401 - insert into Department (caption) values (?)
14:31:59,375 DEBUG StringType:133 - binding 'managers' to parameter: 1
14:31:59,390 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
14:31:59,390 DEBUG StringType:133 - binding 'tom' to parameter: 1
14:31:59,390 DEBUG IntegerType:133 - binding '1' to parameter: 2
14:31:59,390 DEBUG SQL:401 - insert into Department (caption) values (?)
14:31:59,390 DEBUG StringType:133 - binding 'designers' to parameter: 1
14:31:59,390 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
14:31:59,390 DEBUG StringType:133 - binding 'ron' to parameter: 1
14:31:59,390 DEBUG IntegerType:133 - binding '2' to parameter: 2
14:31:59,406 DEBUG SQL:401 - insert into Employee (fio, fk_department_id) values (?, ?)
14:31:59,406 DEBUG StringType:133 - binding 'jim' to parameter: 1
14:31:59,406 DEBUG IntegerType:133 - binding '1' to parameter: 2

Ха, как и ожидалось. При сохранении Джима, мы начали сохранять его отдел (менеджеров). В отделе менеджеры мы нашли Тома (с ним все просто) и Рона. А вот сохраняя Рона, мы начали с того, что сохранили отдел дизайнеров, в который включен Рон, и затем сохраняя самого Рона, установили ему связь на отдел дизайнеров. Тот факт что Рон входит в список сотрудников отдела менеджмент был проигнорирован (спасибо inverse="true").

На этом с сохранением все, переходим к удалению.

ses.beginTransaction();
Department managers_2 =
(Department) ses.load(Department.class, 1);
ses.delete
(managers_2);
ses.getTransaction
().commit();

Смотрим журнал:

14:36:09,343 DEBUG SQL:401 - delete from Employee where employee_id=?
14:36:09,343 DEBUG IntegerType:133 - binding '3' to parameter: 1
14:36:09,359 DEBUG SQL:401 - delete from Employee where employee_id=?
14:36:09,359 DEBUG IntegerType:133 - binding '1' to parameter: 1
14:36:09,359 DEBUG SQL:401 - delete from Employee where employee_id=?
14:36:09,359 DEBUG IntegerType:133 - binding '2' to parameter: 1
14:36:09,359 DEBUG SQL:401 - delete from Department where department_id=?
14:36:09,359 DEBUG IntegerType:133 - binding '1' to parameter: 1

Думаете на этом все, как бы не так – неаккуратных и невнимательных везде ждут сюрпризы. Давайте удалим одного единственного Джима.

ses = factory.openSession();
ses.beginTransaction
();
Employee jim_2 =
(Employee) ses.load(Employee.class, 1);
ses.delete
(jim_2);
ses.getTransaction
().commit();

Смотрим журнал:

14:37:29,031 DEBUG SQL:401 - delete from Employee where employee_id=?
14:37:29,031 DEBUG IntegerType:133 - binding '1' to parameter: 1
14:37:29,031 DEBUG SQL:401 - delete from Employee where employee_id=?
14:37:29,031 DEBUG IntegerType:133 - binding '3' to parameter: 1
14:37:29,031 DEBUG SQL:401 - delete from Employee where employee_id=?
14:37:29,031 DEBUG IntegerType:133 - binding '2' to parameter: 1
14:37:29,046 DEBUG SQL:401 - delete from Department where department_id=?
14:37:29,046 DEBUG IntegerType:133 - binding '1' to parameter: 1

Те же действия, что и в прошлый раз. Дело в том, что мы не осмотрительно поставили каскадные действия на все операции (в том числе и удаление) от сотрудника к отделу. И если сохранение было удобным и полезным, то с удалением мы попали в просак. Удаление Джима инициировало удаление отдела. Удаление отдела привело к тому, что были удалены все его сотрудники. Решением будет изменить отображение многие-к-одному на следующее:

<many-to-one 
    name="department" 
    class="Department" 
    column="fk_department_id" 
    not-null="true" 
    cascade="save-update" 
/>

Проверяем: все работает. На сегодня хватит, спасибо за внимание.



Совсем забыл.

Небольшое дополнение: не забывайте, что при подтверждении транзакции все объекты, которые прошли через сессию (были загружены или созданы) будут проверяться на предмет "истинной духовной чистоты", т.е. были или не были ли они изменены. В последнем примере я удаляю Джима загружая его ОДНОГО из сессии (сессия у меня новая) и ссылка на отдел через сессию не проходит (помним, что по-умолчанию в hibernate свойства many-to-one равно как и коллекции загружаются "лениво"). Поэтому при удалении все получилось отлично. Но вот если сделать так:

ses.beginTransaction();
Department managers_2 =
(Department) ses.load(Department.class, 1);
Set<Employee> employies = managers_2.getEmployies
();
Employee jim = employies.iterator
().next();
ses.delete
(jim);

То возникнет ошибка:

Exception in thread "main" org.hibernate.ObjectDeletedException:
deleted object would be re-saved by cascade
(remove deleted object from associations):
  
[experimental.business.Employee#1]

Т.к. у меня установлено каскадное сохранение всех подчиненных к отделу сотрудников, то hibernate не может удалить сотрудника, ведь на него есть ссылка из отдела (в раннем примере о том, что есть отдел и связь от него к сотрудникам hibernate не догадывался). А раз есть ссылка на сотрудника, то он попросит сотрудника об cascade-сохранении и сотрудник попадет в базу данных. Так что при удалении сотрудника, не забывайте очистить ссылку на его из коллекции сотрудников отдела.

managers_2.getEmployies().remove(jim);

Написав эту строчку я задумался, что фактически можно так настроить hibernate, чтобы он удалил сотрудника из отдела только на основании того факта, что его нет в перечне employee. В начале я планировал опустить вопрос удаления "сирот" и подробнее рассказать о нем в теме посвященной коллекциям, но раз начал, так начал. Среди режимов каскадного распространения "операции" от родительского объекта к ассоциированным с ним (ко всем элементам коллекции или объекту-владельцу), режим cascade="all" не является самым "большим". Есть еще и режим cascade="all-delete-orphan". В докуметации про него говорят так: все что было у cascade="all", плюс, автоматическое удаление всех тех, кто не принадлежит коллекции. Фактически записав ассоциацию так:

<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-delete-orphan" inverse="true"  >
        <key column="fk_department_id"  />
        <one-to-many class="Employee"  />
    </set>
</class>

Я могу удалить сотрудника Джима, следующим образом (обратите внимание, что явного обращения session.delete нет):

ses.beginTransaction();

Department managers_2 =
(Department) ses.load(Department.class, 1);
Set<Employee> employies = managers_2.getEmployies
();
employies.remove
(employies.iterator().next());

ses.getTransaction
().commit();

Использовать или нет такую возможность - ваше лично дело. Хотя я предпочитаю не пользоваться такой функцией. Также хочу заметить, что правила "delete-orphan" оказал влияние не только на удаление, но и на сохранение объектов. К примеру записать что-то вроде этого, не получится: будет ошибка на стадии сохранения графа связанных объектов.

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();

Причина в общем-то дурацкая: когда я сказал сохранить Джима, то hibernate определелил, что нужно сохранить отдел, в котором числится наш Джим. Сохранив отдел, он тут же решил проверить его на "моральную целостность", найти может быть какие-то Сотрудники уже были отчислены из этого отдела (ага, неуспели отдел создать, а сотрудники из него так и бегут). Ну и в ходе этой проверки обнаружил что в отделе числится Джим, который еще не успели сохранить, такого наш hibernate не смог переварить и выбросил исключение.

Exception in thread "main" org.hibernate.TransientObjectException: object references an unsaved transient
instance - save the transient instance before flushing: experimental.business.Employee

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

Получено с http://www.black-zorro.com/mediawiki/