Hibernate: каскадные обновления, инверсия отношений и прочая и прочая
Материал из Dom.
Вот пришло время и мне написать пару строчек про hibernate. Я попробую сделать небольшой cheatsheet по вопросу двусторонней ассоциации, каскадных обновлений, ленивой загрузки и прочего и прочего. Сразу предупрежу, что я довольно негативно отношусь к hibernate, предпочитаю в практике использовать ibatis. Может, причиной является мой опыт в проектировании БД, и я всегда предпочитаю идти именно от базы к модели классов java, а не наоборот. Большинство проблем, которые возникают у новичков заключается в том, что они забывают, что база данных живет по другим правилам, чем слой объектов. В СУБД нет всех этих двусторонних связей, да и в понятие каскадных обновлений вкладывается немного другой смысл. Естественно, что я не исключаю ситуации, “что фокусник был пьян и фокус не удался”, так что ваши замечания будут для меня полезны. Несмотря на то, что я излагаю пример на базе mysql, я полагаю, что основные идеи и выводы будут применимы для любой СУБД. Одним словом, поехали:
Для примера есть такая модель данных из двух таблиц: Сотрудники и Отделы. Отношения между ними в терминах СУБД “один-ко-многим”, где на стороне “один” находится Отдел, а на стороне “много” находится “Сотрудники”. В каждую из табличек я добавлю первичный ключ - id, название отдела/ФИО сотрудника, остальные поля не существенны. Сразу же нужно определиться с тем, будут ли наши сотрудники существовать вне отделов (и это решение очень, очень важное). Предположим, что такое не возможно: сотрудник без отдела тут же удаляется, обратная же ситуация вполне возможна: отдел может существовать без сотрудников сколь угодно долго. Для реализации связи между этим двумя таблицами я должен в таблицу “сотрудники” добавить внешний ключ “fk_department_id”. Надо сказать, что когда я создаю внешний ключ с помощью sql, то могу/скорее должен указать модификаторы этого внешнего ключа. Например, для mysql, этот код будет выглядеть так:
Так я создаю таблицу “отделы”:
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_id' 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 под правила Б.Д. Начнем с того, что поймем, что в базах данных нет двусторонних связей, таких как я спроектировал ранее. Связь реализуется от подчиненного к главному и точка. Например, когда я выполняю типовое назначение сотрудника в отдел, то делаю так:
