Hibernate: Связи вида Многие-ко-Многим и Один-к-Одному

May 21, 2008

Прошлые две статьи были посвящены работе с ассоциацими “один-ко-многим”. Фактически этот вид ассоциаций является наиболее ценным и часто используемым. В теории СУБД (и соответственно, в hibernate) есть еще два вида связей: один-к-одному и многие-ко-многим. Сначала разберем пример, когда могут потребоваться именно такие отношения между таблицами.

В качестве примера связей “один-к-одному” я вспоминаю свое босоногое детство и то, как я писал на старом добром foxpro под ДОС небольшое приложение с табличкой, ну пусть это будет, сотрудник, у которой количество полей просто зашкаливало (их было много, очень много). Я уже точно не помню, что меня подвигло на разделение этого перечня полей на две таблицы с последующим связыванием их как “один-к-одному”, но подобная ситуация является наиболее частой причиной, когда нужно разделять одну логическую сущность на несколько физических таблиц в СУБД и делать между ними связь один-к-одному. Особенно, это полезно в тех случаях, когда часть полей этой мега-таблицы являются редко-используемыми. Например, признак того болел ли данный сотрудник в детстве ветрянкой, пригодится в лучшем случае раз в год. Так что тягать общим “select * from бла-бла-бла” данные из таблицы было глупо. Напоминаю тем, кто не знает, что foxpro - это файловая СУБД и фактически возможности попросить сервер вернуть по сети только нужные поля, без поля с ветрянкой, не было – по сети гонялся весь объем файла с данными. Так, что поддержание небольшого размера файла таблицы - было одним из условий повысить производительность. В hibernate с его идеей избежать ручных select-запросов к СУБД, проблема “зачем мы вытянули из СУБД это гигантское и никому не нужное поле” вернулась. Когда вы работаете с ассоциациями, то можете настроить hibernate получить связанную сущность только по мере необходимости (при первом обращении). Однако, если у вас есть TEXT поле хранящее биографию сотрудника – то это никак на ассоциацию не тянет и легкого способа указать, что поле должно загружаться только по требованию – нет.

На самом деле, я немного лукавлю: такая возможность есть. В hibernate можно пометить некоторое поле как lazy, тогда при первом обращении к сущности оно автоматически загружаться не будет, до тех пор, пока вы явно не обратитесь к значению свойства. Единственный минус в том, что вам нужно после компиляции вашего кода, выполнить его “инструментирование”, в ходе чего байт код меняется и вместо вашего “String biography” подставляется hibernate-proxy. Больших проблем это не вызывает, т.к. подобную процедуру можно включить в состав вашего ant-сценария сборки проекта.
  1. <target name="makeinstrument">
  2.     <taskdef name="instrument"
  3.             classname="org.hibernate.tool.instrument.cglib.InstrumentTask"
  4.             classpathref="library.hibernate.classpath"/>
  5.     <instrument verbose="true" extended="false">
  6.         <fileset dir="${testmach.output.dir}/experimental/business">
  7.            <include name="**/*.class"/>
  8.             </fileset>
  9.     </instrument>
  10. </target>
Предполагается, что classpathref="library.hibernate.classpath" задает информацию о местоположении библиотек нужных для hibernate & cglib. А значение выражения ="${testmach.output.dir}/experimental/business ссылается на каталог с class-файлами которые нужно обработать. Сразу предупреждаю, что применять инструментирование нужно очень аккуратно и целенаправленно: так если применить его для POJO вашей модели данных, то проблем нет, но если заинструментировать код, который сам работает с dynamic proxy или aop, то проблемы будут – проверено.

Теперь давайте внесем небольшие правки в код маппинга для Employee, добавим ему поле fio с функцией “загрузки по требованию”:
  1. <property name="bio" lazy="true" type="text" />
Само собой, что в классе Employee должно было появиться новое поле bio.
  1. StringBuffer bigBio = new StringBuffer();
  2. int __x = 1000;
  3. while (__x-- > 0)
  4.   bigBio.append("bigbuf");
  5.  
  6. Employee lena = new Employee("lena");
  7. lena.setBio(bigBio.toString());
  8.  
  9. lena.setDepartment(designers);
  10.  
  11. ….
  12.  
  13. Employee lena_2 = (Employee) ses.load(Employee.class, 1);
  14. … какие-то действия …
  15. System.out.println (lena.getBio ());
А вот журнал выполнения приложения (как видите, изначальный запрос select не включал в себя поле bio, и только спустя некоторое время был выполнен второй запрос только для одного поля bio):
21:05:26,656 DEBUG SQL:401 - select employies0_.fk_department_id as fk3_1_, 
employies0_.employee_id as employee1_1_, employies0_.employee_id as employee1_3_0_, 
employies0_.fio as fio3_0_, employies0_.fk_department_id as fk3_3_0_ 
from Employee employies0_ where employies0_.fk_department_id=?
 
 
21:06:48,125 DEBUG SQL:401 - select employee_.bio as bio3_ from Employee employee_ where employee_.employee_id=?
21:06:48,125 DEBUG IntegerType:133 - binding '72' to parameter: 1
21:06:48,140 DEBUG TextType:172 - returning 'bigbufbi .... bigbuf' as column: bio3_
Теперь поспотрим как можно реализовать связь один-к-одному с использование действительно двух таблиц. Для этого я добавлю нашему сотруднику дополнительное поле car. Хранящее ссылку на автомобиль сотрудника (вот такая у нас счастливая страна, что у каждого работника есть свой автомобиль, или наоборот, учитывая что у работника может быть не более одного автомобильчика). Мне нужен класс Car со следующим полями:
  1. /**
  2.  * Автомобиль сотрудника.
  3.  */
  4. public class Car {
  5.     // идентификатор автомобиля
  6.     Integer car_id;
  7.  
  8.     // название модели авто
  9.     String model;
  10.     // и дата его выпуска
  11.     Calendar productionDate;
  12.  
  13.     // также мы добавляем ссылку на сотрудника владеющего данным авто
  14.     Employee employee;
  15.  
  16.     public Car() {
  17.     }
  18.  
  19.     public Car(String model, Calendar productionDate) {
  20.         this.model = model;
  21.         this.productionDate = productionDate;
  22.     }
  23.  
  24.     public Integer getCar_id() {
  25.         return car_id;
  26.     }
  27.  
  28.     public void setCar_id(Integer car_id) {
  29.         this.car_id = car_id;
  30.     }
  31.  
  32.     public String getModel() {
  33.         return model;
  34.     }
  35.  
  36.     public void setModel(String model) {
  37.         this.model = model;
  38.     }
  39.  
  40.     public Calendar getProductionDate() {
  41.         return productionDate;
  42.     }
  43.  
  44.     public void setProductionDate(Calendar productionDate) {
  45.         this.productionDate = productionDate;
  46.     }
  47.  
  48.     public Employee getEmployee() {
  49.         return employee;
  50.     }
  51.  
  52.     public void setEmployee(Employee employee) {
  53.         this.employee = employee;
  54.     }
  55. }
Как видите, ничего не обычного: разве что я добавил поле employee для ссылки на сотрудника владеющего данным авто. Теперь смотрим, какие изменения произошли в коде класса сотрудника:
  1. /**
  2.  * Сотрудник
  3.  */
  4. public class Employee {
  5.     Integer employee_id;
  6.     String fio;
  7.     Department department;
  8.     Integer uo;
  9.     String bio;
  10.  
  11.     // автомобиль работника
  12.     Car car;
  13.     public Car getCar() {
  14.         return car;
  15.     }
  16.     public void setCar(Car car) {
  17.         this.car = car;
  18.     }
  19. ……
  20. }
Теперь пример файлов маппинга:
  1. <class name="Employee" dynamic-insert="true" dynamic-update="true">
  2.    <id name="employee_id">
  3.        <generator class="native"/>
  4.     </id>
  5.     <property name="fio"/>
  6.     <many-to-one name="department" class="Department" column="fk_department_id" not-null="true"
  7.                cascade="save-update"/>
  8.  
  9.     <property name="bio" lazy="true" type="text" />
  10.     <one-to-one name="car" class="Car" property-ref="employee" cascade="all" />
  11.  
  12. </class>
И второй файл для автомобиля:
  1. <class name="Car" dynamic-insert="true" dynamic-update="true">
  2.   <id name="car_id">
  3.      <generator class="native"/>
  4.   </id>
  5.   <property name="model"/>
  6.   <property name="productionDate" type="calendar"/>
  7.  
  8.   <!-- важно что данное поле должно быть уникальным -->
  9.   <many-to-one name="employee" class="Employee"
  10.        column="fk_employee_id" cascade="all"
  11.        unique="true"/>
  12. </class>
Теперь краткий анализ: на самом деле я сделал небольшой финт ушами – у таблицы car есть свой, вполне таки обычный первичный ключ в виде автосчетчика. Который, в общем случае, никак не зависит от значения первичного ключа во второй таблице – сотруднике. Я имитирую связь один-к-одному с помощью внедрения в состав таблицы Авто внешнего ключа ссылающегося на таблицу сотрудников (fk_employee_id). Важно, что я должен пометить данный ключ как unique, иначе на уровне базы данных вполне возможна ситуация, что у какого-то сотрудника окажется сразу два автомобиля. На стороне сотрудника я использую специализированный тег one-to-one, в качестве атрибутов для него я должен указать то, как называется свойство в классе-партнере property-ref="employee".

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

Теперь пример использования:
  1. Employee lena = new Employee("lena");
  2.  
  3. Calendar calen_mazzzda = new GregorianCalendar();
  4. calen_mazzzda.set(2006, 1, 1);
  5. Car mazzzda = new Car("mazzzda", calen_mazzzda);
  6. lena.setCar(mazzzda);
  7. mazzzda.setEmployee(lena);
  8. lena.setDepartment(designers);
  9.  
  10.  
  11. // любое из этих двух действий приведет у уничтожению связанной записи
  12. //ses.delete(lena_2);
  13. ses.delete(lena_2.getCar());
Второй способ организации связи один-к-одному не предполагает внедрение в одну из таблиц внешнего ключа. Более того, теперь у второй таблицы нет собственного независимого генератора значений первичного ключа – эти величины должны совпадать для обоих объектов. Никаких изменений в коде java-классов эта демонстрация не требует, основные изменения только в файле маппинга:
  1. <class name="Employee" dynamic-insert="true" dynamic-update="true">
  2.    <id name="employee_id">
  3.       <generator class="native"/>
  4.    </id>
  5.    <property name="fio"/>
  6.    <many-to-one name="department" class="Department" column="fk_department_id" not-null="true"
  7.          cascade="save-update"/>
  8.  
  9.    <property name="bio" lazy="true" type="text" />
  10.    <one-to-one name="car" class="Car" cascade="all" />
  11.  
  12. </class>
И теперь пример файла маппинга для Авто:
  1. <class name="Car" dynamic-insert="true" dynamic-update="true">
  2.    <id name="car_id">
  3.        <generator class="foreign">
  4.           <param name="property">employee</param>
  5.       </generator>
  6.    </id>
  7.    <property name="model"/>
  8.    <property name="productionDate" type="calendar"/>
  9.  
  10.    <one-to-one name="employee" class="Employee" constrained="true" cascade="all"/>
  11. </class>
Как видите, здесь изменений больше: прежде всего я изменил запись управлющую первичным ключом сущности. Теперь генерация выполняется не средствами СУБД а подставляется самим hibernate на основании значения идентификатора того объекта (сотрудника, конечно), который хранится в поле employee. Также в теге one-to-one мне нужно записать атрибут constrainded=true.

На этом про связи один-к-одному хватит и переходим к связям многие-ко-многим. Все вы знаете, что на уровне СУБД такие связи создаются только с помощью промежуточной таблицы. Например связь между людьми и газетами (люди подписываются на газеты, очевидно, что один человек может читать много газет, также как и одно издание может выписываться множеством людей). так вот такая связь предполагает наличие промежуточной таблицы employee2newspaper, содержащей как минимум два поля: идентификаторы читателя-сотрудника и газеты. И вот в этом слове “как минимум” и кроется корень зла. Дело в том, что я за свою практику, за исключением крайне надуманных ситуаций, не встречался с тем, что промежуточная таблица была только из двух полей, всегда приходилось сразу или попозже добавлять характеристики связи: длительность подписки, стоимость доставки … Таким образом в реальности связь многие-ко-многим трансформировалась в выделение трех классов, трех файлов маппинга и двух последовательных соединений между ними в виде один-ко-многим и еще раз один-ко-многим. Если же случится такое чудо, что вам не нужны параметры связи то можно сделать так:

Я создаю новый класс Газеты:
  1. /**
  2.  * Газета, которую будут выписывать наши сотрудники
  3.  */
  4. public class NewsPaper {
  5.     // идентификатор газеты
  6.     Integer newspaper_id;
  7.     // название газеты
  8.     String caption;
  9.  
  10.     Set<Employee> readers = new HashSet<Employee>();
  11.  
  12.     public NewsPaper() {
  13.     }
  14.  
  15.     public NewsPaper(String caption) {
  16.         this.caption = caption;
  17.     }
  18.  
  19.     public Integer getNewspaper_id() {
  20.         return newspaper_id;
  21.     }
  22.  
  23.     public void setNewspaper_id(Integer newspaper_id) {
  24.         this.newspaper_id = newspaper_id;
  25.     }
  26.  
  27.     public String getCaption() {
  28.         return caption;
  29.     }
  30.  
  31.     public void setCaption(String caption) {
  32.         this.caption = caption;
  33.     }
  34.  
  35.     public Set<Employee> getReaders() {
  36.         return readers;
  37.     }
  38.  
  39.     public void setReaders(Set<Employee> readers) {
  40.         this.readers = readers;
  41.     }
  42. }
В состав класса Сотрудник добавляется новая коллекция newspapers:
  1. /**
  2.  * Сотрудник
  3.  */
  4. public class Employee {
  5.     Integer employee_id;
  6.     String fio;
  7.     Department department;
  8.     Integer uo;
  9.     String bio;
  10.  
  11.     // газеты, которые читает наш работник вместо того, чтобы приносить боссу бабло
  12.     Set <NewsPaper> newspapers = new HashSet<NewsPaper>();
  13.     public Set<NewsPaper> getNewspapers() {
  14.         return newspapers;
  15.     }
  16.     public void setNewspapers(Set<NewsPaper> newspapers) {
  17.         this.newspapers = newspapers;
  18.     }
  19.  
  20.  ….
  21. }
Теперь нужно переписать файлы маппинга, сначала файл для газеты:
  1. <class name="NewsPaper" dynamic-insert="true" dynamic-update="true">
  2.    <id name="newspaper_id">
  3.         <generator class="native"/>
  4.    </id>
  5.    <property name="caption"/>
  6.  
  7.    <set name="readers" cascade="save-update" inverse="true" table="employee2newspaper">
  8.      <key column="fk_newspaper_id" not-null="true" />
  9.      <many-to-many class="Employee" column="fk_employee_id"  />
  10.   </set>
  11. </class>
Основное внимание на запись set. Я должен указать имя промежуточной таблицы, которая будет связывать моих читателей и газеты. Затем я указываю как будут называться поля играющие роль внешних ключей – со стороны газеты, поле будет называться fk_newspaper_id (очевидно, что оно не должно хранить null значения). На стороне читателя поле будет называться fk_employee_id.

Изменения в файле маппинга для сотрудника синхронны:
  1. <class name="Employee" dynamic-insert="true" dynamic-update="true">
  2.   <id name="employee_id">
  3.     <generator class="native"/>
  4.   </id>
  5.   <property name="fio"/>
  6.   <many-to-one name="department" class="Department" column="fk_department_id" not-null="true"
  7.         cascade="save-update"/>
  8.   <property name="bio" lazy="true" type="text" />
  9.   <one-to-one name="car" class="Car" cascade="all" />
  10.  
  11.   <set name="newspapers" cascade="save-update" table="employee2newspaper">
  12.     <key column="fk_employee_id" />
  13.     <many-to-many class="NewsPaper" column="fk_newspaper_id" />
  14.   </set>
  15. </class>
Я указал те же названия промежуточной таблицы и служебных полей. Основное внимание на то, что одну из сторон связи я должен пометить как inverse=”true”, в противном случае мы столкнемся со всеми проблемами, о которых я упоминал еще в первой статье. Еще очень важно то, что я для обоих сторон установил каскадные операции как “save-update” – никаких all или delete. Дело в том, что если я так не сделаю, то при попытке удалить сотрудника удалению подвергнутся все газеты которые он выписывают и удалены они будут не только из промежуточной таблицы, но и из главной таблицы, справочника газет – оно нам нужно?

В результате будет сгенерирована следующая структура данных:
  1. CREATE TABLE `employee2newspaper` (
  2.   `fk_employee_id` int(11) NOT NULL,
  3.   `fk_newspaper_id` int(11) NOT NULL,
  4.   PRIMARY KEY (`fk_employee_id`,`fk_newspaper_id`),
  5.   KEY `FKB9DE4FD5632D9988` (`fk_employee_id`),
  6.   KEY `FKB9DE4FD55575D22C` (`fk_newspaper_id`),
  7.   CONSTRAINT `FKB9DE4FD55575D22C` FOREIGN KEY (`fk_newspaper_id`) REFERENCES `newspaper` (`newspaper_id`),
  8.   CONSTRAINT `FKB9DE4FD5632D9988` FOREIGN KEY (`fk_employee_id`) REFERENCES `employee` (`employee_id`)
  9. ) ENGINE=InnoDB
Теперь пример использования:
  1. Employee jim = new Employee("jim");
  2. Employee tom = new Employee("tom");
  3. Employee ron = new Employee("ron");
  4. Department managers = new Department("managers");
  5. Department designers = new Department("designers");
  6.  
  7. jim.setDepartment(managers);
  8. tom.setDepartment(managers);
  9. ron.setDepartment(managers);
  10.  
  11. Employee lena = new Employee("lena");
  12.  
  13. Calendar calen_mazzzda = new GregorianCalendar();
  14. calen_mazzzda.set(2006, 1, 1);
  15. Car mazzzda = new Car("mazzzda", calen_mazzzda);
  16. lena.setCar(mazzzda);
  17. mazzzda.setEmployee(lena);
  18. lena.setDepartment(designers);
  19.  
  20. NewsPaper pravda = new NewsPaper("pravda");
  21. NewsPaper trud = new NewsPaper("trud");
  22. NewsPaper znamia_lenina = new NewsPaper("znamia_lenina");
  23.  
  24. // заметьте что эти действия будут проигнорированы т.к.
  25. // они выполняются на стороне не управляющей связью
  26. pravda.getReaders().add(lena);
  27. pravda.getReaders().add(jim);
  28. trud.getReaders().add(ron);
  29. trud.getReaders().add(tom);
  30.  
  31. // а эти операции сохранения будут успешными
  32. jim.getNewspapers().add(pravda);
  33. ron.getNewspapers().add(pravda);
  34. lena.getNewspapers().add(pravda);
  35. lena.getNewspapers().add(trud);
  36. lena.getNewspapers().add(znamia_lenina);
  37.  
  38.  
  39. ----
  40.  
  41. ses.beginTransaction();
  42.  
  43. Department managers_2 = (Department) ses.load(Department.class, 1);
  44. Employee jim_2 = (Employee) ses.load(Employee.class, jim.getEmployee_id());
  45. Employee lena_2 = (Employee) ses.load(Employee.class, lena.getEmployee_id());
  46. NewsPaper pravda_2 = (NewsPaper) ses.load(NewsPaper.class, pravda.getNewspaper_id());
  47.  
  48.  
  49. // выполнить удаление газеты мы не можем т.к. у нее есть два читателя
  50. // и это нарушает ограничения на уровне базы данных
  51. // так что единственный способ выполнить удаление газеты и всей связанной информации
  52. // - использовать sql&hql
  53. //ses.delete(pravda_2);
  54.  
  55. // а вот выполнить удаление сотрудника-читателя мы можем т.к. hibernate знает,
  56. // что предварительно нужно удалить все записи в связывающей таблице
  57. ses.delete(lena_2);
  58.  
  59. // и второй способ удаления – когда удаляется только одна запись 
  60. // в промежуточной таблице. И сотрудник и газеты при этом никуда не исчезают.
  61. lena_2.getNewspapers().remove(pravda_2);
Общие замечания: напоминаю, что лишь те изменения, которые были сделаны на “владеющей стороне” (не помеченной как inverse будут учитываться как при добавлении данных, так и при их очистке). Прочитайте также в тексте программы комментарии по поводу удаления связанных сущностей.

Что касается подбора возможных видов коллекций для хранения перечня газет или сотрудников, то можно использовать не только set, но и bag, idbag, map, list. Нужно только помнить, что есть ограничения. Если мы говорим о стороне управляющей связью, то проблем нет, и можно использовать любую из перечисленных выше коллекций, но как только мы упоминаем сторону, не владеющую связью, то из перечня допустимых вариантов выпадают map и list. Вспомните, как в прошлой статье я рассказывал о том, что не владеющая сторона не может устанавливать значение ни индекса, ни ключа для map.

Есть еще один особый вариант организации связи многие-ко-многим. Когда мы не используем промежуточную сущность (отдельный java-класс замапленный на таблицу), но при этом пользуемся возможностью “присоединить к каждой из связей” некоторые атрибуты. Для этого нужно использовать компонент. Ведь в качестве его полей могут быть и ассоциации:

Сначала пример класса Подписка:
  1. /**
  2.  * Промежуточный класс хранящий информацию об подписке на некоторые издания
  3.  */
  4. public class Subscription {
  5.     Integer subscription_id;
  6.  
  7.     Employee employee;
  8.     NewsPaper newspaper;
  9.  
  10.     Calendar dateof;
  11.  
  12.     public Subscription() {
  13.     }
  14.  
  15.     public Subscription(Employee employee, NewsPaper newspaper, Calendar dateof) {
  16.         this.employee = employee;
  17.         this.newspaper = newspaper;
  18.         this.dateof = dateof;
  19.     }
  20.  
  21.     public Integer getSubscription_id() {
  22.         return subscription_id;
  23.     }
  24.  
  25.     public void setSubscription_id(Integer subscription_id) {
  26.         this.subscription_id = subscription_id;
  27.     }
  28.  
  29.     public Employee getEmployee() {
  30.         return employee;
  31.     }
  32.  
  33.     public void setEmployee(Employee employee) {
  34.         this.employee = employee;
  35.     }
  36.  
  37.     public NewsPaper getNewspaper() {
  38.         return newspaper;
  39.     }
  40.  
  41.     public void setNewspaper(NewsPaper newspaper) {
  42.         this.newspaper = newspaper;
  43.     }
  44.  
  45.     public Calendar getDateof() {
  46.         return dateof;
  47.     }
  48.  
  49.     public void setDateof(Calendar dateof) {
  50.         this.dateof = dateof;
  51.     }
  52. }
Для этого файла нет никакого маппинга – он используется как компонент. Теперь примеры файлов маппинга для сотрудника и газеты. Опять таки обратите внимание, что каскадные операции у меня уже не включают удаление связанной записи (на высшем уровне). Что касается каскадов для вложенных ассоциаций (many-to-one), то их значение является несущественным т.к. если каскадного удаления нет для родителя, то и до вложенного элемента оно не дойдет:
  1. <class name="Employee" dynamic-INSERT="true" dynamic-UPDATE="true" >
  2.    <id name="employee_id">
  3.        <generator class="native"/>
  4.    </id>
  5.    <property name="fio"/>
  6.    <many-to-one name="department" class="Department" COLUMN="fk_department_id" not-NULL="true"
  7.                 cascade="save-update"/>
  8.  
  9.    <property name="bio" lazy="true" type="text"/>
  10.    <one-to-one name="car" class="Car" cascade="all"/>
  11.    <set name="subscriptions" cascade="save-update" inverse="false" TABLE="employee2newspaper" >
  12.        <key COLUMN="fk_employee_id" not-NULL="true"/>
  13.        <composite-element class="Subscription" >
  14.           <parent name="employee" />
  15.           <property name="dateof" type="calendar" not-NULL="true"/>
  16.           <many-to-one name="newspaper" class="NewsPaper" COLUMN="fk_newspaper_id" not-NULL="true" cascade="save-update"/>
  17.        </composite-element>
  18.    </set>
  19. </class>
А это пример маппинга для Газеты:
  1. <class name="NewsPaper" dynamic-insert="true" dynamic-update="true">
  2.    <id name="newspaper_id">
  3.       <generator class="native"/>
  4.    </id>
  5.    <property name="caption"/>
  6.  
  7.    <set name="subscriptions" cascade="save-update" inverse="true" table="employee2newspaper">
  8.       <key column="fk_newspaper_id" not-null="true"/>
  9.  
  10.       <composite-element class="Subscription">
  11.          <parent name="newspaper"/>
  12.          <property name="dateof" type="calendar" not-null="true"/>
  13.          <many-to-one name="employee" class="Employee" column="fk_employee_id" not-null="true"  cascade="save-update"/>
  14.       </composite-element>
  15.    </set>
  16. </class>
Теперь я привожу пример структуры данных для сгенерированной hibernate промежуточной таблицы:
  1. CREATE TABLE `employee2newspaper` (
  2.   `fk_employee_id` int(11) NOT NULL,
  3.   `dateof` datetime DEFAULT NULL,
  4.   `fk_newspaper_id` int(11) NOT NULL,
  5.   KEY `FKB9DE4FD5632D9988` (`fk_employee_id`),
  6.   KEY `FKB9DE4FD55575D22C` (`fk_newspaper_id`),
  7.   CONSTRAINT `FKB9DE4FD55575D22C` FOREIGN KEY (`fk_newspaper_id`) REFERENCES `newspaper` (`newspaper_id`),
  8.   CONSTRAINT `FKB9DE4FD5632D9988` FOREIGN KEY (`fk_employee_id`) REFERENCES `employee` (`employee_id`)
  9. ) ENGINE=InnoDB
Обратите внимание на то, что здесь в таблице нет первичного ключа. Надо сказать, что здесь находится не слабенькая дырка в системе безопасности т.к. вполне возможны дубляжи, когда один человек (Джим) будет подписан на газету (Правда) несколько раз и даже на одну и туже дату. В принципе, когда создается связывающая таблица, то hibernate создает первичный ключ, только тогда, когда все поля входящие в состав такого класса помечены как not null. Это не сделано для поля хранящего дату подписки. К сожалению, я никак не могу добиться подобного поведения. Заметка для себя: перечитать избранные главы документации по hibernate.

Здесь же возникает еще один интересный вопрос о том, как запретить положить в set идентичные пары Читатель-Газета, фактически нам нужно реализовать методы equals & hashCode, но делать это нужно аккуратно т.к. подводных камней хватает.

Categories: Java