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_
« Системы управления версиями для программистов и не только. Часть 5 | Тестируй веб-сайты вместе с Badboy. Часть 1 » |
Hibernate: Связи вида Многие-ко-Многим и Один-к-Одному
Прошлые две статьи были посвящены работе с ассоциацими “один-ко-многим”. Фактически этот вид ассоциаций является наиболее ценным и часто используемым. В теории СУБД (и соответственно, в hibernate) есть еще два вида связей: один-к-одному и многие-ко-многим. Сначала разберем пример, когда могут потребоваться именно такие отношения между таблицами.В качестве примера связей “один-к-одному” я вспоминаю свое босоногое детство и то, как я писал на старом добром foxpro под ДОС небольшое приложение с табличкой, ну пусть это будет, сотрудник, у которой количество полей просто зашкаливало (их было много, очень много). Я уже точно не помню, что меня подвигло на разделение этого перечня полей на две таблицы с последующим связыванием их как “один-к-одному”, но подобная ситуация является наиболее частой причиной, когда нужно разделять одну логическую сущность на несколько физических таблиц в СУБД и делать между ними связь один-к-одному. Особенно, это полезно в тех случаях, когда часть полей этой мега-таблицы являются редко-используемыми. Например, признак того болел ли данный сотрудник в детстве ветрянкой, пригодится в лучшем случае раз в год. Так что тягать общим “select * from бла-бла-бла” данные из таблицы было глупо. Напоминаю тем, кто не знает, что foxpro - это файловая СУБД и фактически возможности попросить сервер вернуть по сети только нужные поля, без поля с ветрянкой, не было – по сети гонялся весь объем файла с данными. Так, что поддержание небольшого размера файла таблицы - было одним из условий повысить производительность. В hibernate с его идеей избежать ручных select-запросов к СУБД, проблема “зачем мы вытянули из СУБД это гигантское и никому не нужное поле” вернулась. Когда вы работаете с ассоциациями, то можете настроить hibernate получить связанную сущность только по мере необходимости (при первом обращении). Однако, если у вас есть TEXT поле хранящее биографию сотрудника – то это никак на ассоциацию не тянет и легкого способа указать, что поле должно загружаться только по требованию – нет.
На самом деле, я немного лукавлю: такая возможность есть. В hibernate можно пометить некоторое поле как lazy, тогда при первом обращении к сущности оно автоматически загружаться не будет, до тех пор, пока вы явно не обратитесь к значению свойства. Единственный минус в том, что вам нужно после компиляции вашего кода, выполнить его “инструментирование”, в ходе чего байт код меняется и вместо вашего “String biography” подставляется hibernate-proxy. Больших проблем это не вызывает, т.к. подобную процедуру можно включить в состав вашего ant-сценария сборки проекта.
<target name="makeinstrument">
<taskdef name="instrument"
classname="org.hibernate.tool.instrument.cglib.InstrumentTask"
classpathref="library.hibernate.classpath"/>
<instrument verbose="true" extended="false">
<fileset dir="${testmach.output.dir}/experimental/business">
<include name="**/*.class"/>
</fileset>
</instrument>
</target>
Теперь давайте внесем небольшие правки в код маппинга для Employee, добавим ему поле fio с функцией “загрузки по требованию”:
<property name="bio" lazy="true" type="text" />
StringBuffer bigBio = new StringBuffer();
int __x = 1000;
while (__x-- > 0)
bigBio.append("bigbuf");
Employee lena = new Employee("lena");
lena.setBio(bigBio.toString());
lena.setDepartment(designers);
….
Employee lena_2 = (Employee) ses.load(Employee.class, 1);
… какие-то действия …
System.out.println (lena.getBio ());
/**
* Автомобиль сотрудника.
*/
public class Car {
// идентификатор автомобиля
Integer car_id;
// название модели авто
String model;
// и дата его выпуска
Calendar productionDate;
// также мы добавляем ссылку на сотрудника владеющего данным авто
Employee employee;
public Car() {
}
public Car(String model, Calendar productionDate) {
this.model = model;
this.productionDate = productionDate;
}
public Integer getCar_id() {
return car_id;
}
public void setCar_id(Integer car_id) {
this.car_id = car_id;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public Calendar getProductionDate() {
return productionDate;
}
public void setProductionDate(Calendar productionDate) {
this.productionDate = productionDate;
}
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
}
/**
* Сотрудник
*/
public class Employee {
Integer employee_id;
String fio;
Department department;
Integer uo;
String bio;
// автомобиль работника
Car car;
public Car getCar() {
return car;
}
public void setCar(Car car) {
this.car = car;
}
……
}
<class name="Employee" dynamic-insert="true" dynamic-update="true">
<id name="employee_id">
<generator class="native"/>
</id>
<property name="fio"/>
<many-to-one name="department" class="Department" column="fk_department_id" not-null="true"
cascade="save-update"/>
<property name="bio" lazy="true" type="text" />
<one-to-one name="car" class="Car" property-ref="employee" cascade="all" />
</class>
<class name="Car" dynamic-insert="true" dynamic-update="true">
<id name="car_id">
<generator class="native"/>
</id>
<property name="model"/>
<property name="productionDate" type="calendar"/>
<!-- важно что данное поле должно быть уникальным -->
<many-to-one name="employee" class="Employee"
column="fk_employee_id" cascade="all"
unique="true"/>
</class>
Каскадные операции для обоих сторон установлены как all, т.к. вполне очевидно, что удаление одной записи должно приводить к автоматическому уничтожению другой (типа, разбил авто – и тебя продали на органы, вот так-то).
Теперь пример использования:
Employee lena = new Employee("lena");
Calendar calen_mazzzda = new GregorianCalendar();
calen_mazzzda.set(2006, 1, 1);
Car mazzzda = new Car("mazzzda", calen_mazzzda);
lena.setCar(mazzzda);
mazzzda.setEmployee(lena);
lena.setDepartment(designers);
// любое из этих двух действий приведет у уничтожению связанной записи
//ses.delete(lena_2);
ses.delete(lena_2.getCar());
<class name="Employee" dynamic-insert="true" dynamic-update="true">
<id name="employee_id">
<generator class="native"/>
</id>
<property name="fio"/>
<many-to-one name="department" class="Department" column="fk_department_id" not-null="true"
cascade="save-update"/>
<property name="bio" lazy="true" type="text" />
<one-to-one name="car" class="Car" cascade="all" />
</class>
<class name="Car" dynamic-insert="true" dynamic-update="true">
<id name="car_id">
<generator class="foreign">
<param name="property">employee</param>
</generator>
</id>
<property name="model"/>
<property name="productionDate" type="calendar"/>
<one-to-one name="employee" class="Employee" constrained="true" cascade="all"/>
</class>
На этом про связи один-к-одному хватит и переходим к связям многие-ко-многим. Все вы знаете, что на уровне СУБД такие связи создаются только с помощью промежуточной таблицы. Например связь между людьми и газетами (люди подписываются на газеты, очевидно, что один человек может читать много газет, также как и одно издание может выписываться множеством людей). так вот такая связь предполагает наличие промежуточной таблицы employee2newspaper, содержащей как минимум два поля: идентификаторы читателя-сотрудника и газеты. И вот в этом слове “как минимум” и кроется корень зла. Дело в том, что я за свою практику, за исключением крайне надуманных ситуаций, не встречался с тем, что промежуточная таблица была только из двух полей, всегда приходилось сразу или попозже добавлять характеристики связи: длительность подписки, стоимость доставки … Таким образом в реальности связь многие-ко-многим трансформировалась в выделение трех классов, трех файлов маппинга и двух последовательных соединений между ними в виде один-ко-многим и еще раз один-ко-многим. Если же случится такое чудо, что вам не нужны параметры связи то можно сделать так:
Я создаю новый класс Газеты:
/**
* Газета, которую будут выписывать наши сотрудники
*/
public class NewsPaper {
// идентификатор газеты
Integer newspaper_id;
// название газеты
String caption;
Set<Employee> readers = new HashSet<Employee>();
public NewsPaper() {
}
public NewsPaper(String caption) {
this.caption = caption;
}
public Integer getNewspaper_id() {
return newspaper_id;
}
public void setNewspaper_id(Integer newspaper_id) {
this.newspaper_id = newspaper_id;
}
public String getCaption() {
return caption;
}
public void setCaption(String caption) {
this.caption = caption;
}
public Set<Employee> getReaders() {
return readers;
}
public void setReaders(Set<Employee> readers) {
this.readers = readers;
}
}
/**
* Сотрудник
*/
public class Employee {
Integer employee_id;
String fio;
Department department;
Integer uo;
String bio;
// газеты, которые читает наш работник вместо того, чтобы приносить боссу бабло
Set <NewsPaper> newspapers = new HashSet<NewsPaper>();
public Set<NewsPaper> getNewspapers() {
return newspapers;
}
public void setNewspapers(Set<NewsPaper> newspapers) {
this.newspapers = newspapers;
}
….
}
<class name="NewsPaper" dynamic-insert="true" dynamic-update="true">
<id name="newspaper_id">
<generator class="native"/>
</id>
<property name="caption"/>
<set name="readers" cascade="save-update" inverse="true" table="employee2newspaper">
<key column="fk_newspaper_id" not-null="true" />
<many-to-many class="Employee" column="fk_employee_id" />
</set>
</class>
Изменения в файле маппинга для сотрудника синхронны:
<class name="Employee" dynamic-insert="true" dynamic-update="true">
<id name="employee_id">
<generator class="native"/>
</id>
<property name="fio"/>
<many-to-one name="department" class="Department" column="fk_department_id" not-null="true"
cascade="save-update"/>
<property name="bio" lazy="true" type="text" />
<one-to-one name="car" class="Car" cascade="all" />
<set name="newspapers" cascade="save-update" table="employee2newspaper">
<key column="fk_employee_id" />
<many-to-many class="NewsPaper" column="fk_newspaper_id" />
</set>
</class>
В результате будет сгенерирована следующая структура данных:
CREATE TABLE `employee2newspaper` (
`fk_employee_id` int(11) NOT NULL,
`fk_newspaper_id` int(11) NOT NULL,
PRIMARY KEY (`fk_employee_id`,`fk_newspaper_id`),
KEY `FKB9DE4FD5632D9988` (`fk_employee_id`),
KEY `FKB9DE4FD55575D22C` (`fk_newspaper_id`),
CONSTRAINT `FKB9DE4FD55575D22C` FOREIGN KEY (`fk_newspaper_id`) REFERENCES `newspaper` (`newspaper_id`),
CONSTRAINT `FKB9DE4FD5632D9988` FOREIGN KEY (`fk_employee_id`) REFERENCES `employee` (`employee_id`)
) ENGINE=InnoDB
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(managers);
Employee lena = new Employee("lena");
Calendar calen_mazzzda = new GregorianCalendar();
calen_mazzzda.set(2006, 1, 1);
Car mazzzda = new Car("mazzzda", calen_mazzzda);
lena.setCar(mazzzda);
mazzzda.setEmployee(lena);
lena.setDepartment(designers);
NewsPaper pravda = new NewsPaper("pravda");
NewsPaper trud = new NewsPaper("trud");
NewsPaper znamia_lenina = new NewsPaper("znamia_lenina");
// заметьте что эти действия будут проигнорированы т.к.
// они выполняются на стороне не управляющей связью
pravda.getReaders().add(lena);
pravda.getReaders().add(jim);
trud.getReaders().add(ron);
trud.getReaders().add(tom);
// а эти операции сохранения будут успешными
jim.getNewspapers().add(pravda);
ron.getNewspapers().add(pravda);
lena.getNewspapers().add(pravda);
lena.getNewspapers().add(trud);
lena.getNewspapers().add(znamia_lenina);
----
ses.beginTransaction();
Department managers_2 = (Department) ses.load(Department.class, 1);
Employee jim_2 = (Employee) ses.load(Employee.class, jim.getEmployee_id());
Employee lena_2 = (Employee) ses.load(Employee.class, lena.getEmployee_id());
NewsPaper pravda_2 = (NewsPaper) ses.load(NewsPaper.class, pravda.getNewspaper_id());
// выполнить удаление газеты мы не можем т.к. у нее есть два читателя
// и это нарушает ограничения на уровне базы данных
// так что единственный способ выполнить удаление газеты и всей связанной информации
// - использовать sql&hql
//ses.delete(pravda_2);
// а вот выполнить удаление сотрудника-читателя мы можем т.к. hibernate знает,
// что предварительно нужно удалить все записи в связывающей таблице
ses.delete(lena_2);
// и второй способ удаления – когда удаляется только одна запись
// в промежуточной таблице. И сотрудник и газеты при этом никуда не исчезают.
lena_2.getNewspapers().remove(pravda_2);
Что касается подбора возможных видов коллекций для хранения перечня газет или сотрудников, то можно использовать не только set, но и bag, idbag, map, list. Нужно только помнить, что есть ограничения. Если мы говорим о стороне управляющей связью, то проблем нет, и можно использовать любую из перечисленных выше коллекций, но как только мы упоминаем сторону, не владеющую связью, то из перечня допустимых вариантов выпадают map и list. Вспомните, как в прошлой статье я рассказывал о том, что не владеющая сторона не может устанавливать значение ни индекса, ни ключа для map.
Есть еще один особый вариант организации связи многие-ко-многим. Когда мы не используем промежуточную сущность (отдельный java-класс замапленный на таблицу), но при этом пользуемся возможностью “присоединить к каждой из связей” некоторые атрибуты. Для этого нужно использовать компонент. Ведь в качестве его полей могут быть и ассоциации:
Сначала пример класса Подписка:
/**
* Промежуточный класс хранящий информацию об подписке на некоторые издания
*/
public class Subscription {
Integer subscription_id;
Employee employee;
NewsPaper newspaper;
Calendar dateof;
public Subscription() {
}
public Subscription(Employee employee, NewsPaper newspaper, Calendar dateof) {
this.employee = employee;
this.newspaper = newspaper;
this.dateof = dateof;
}
public Integer getSubscription_id() {
return subscription_id;
}
public void setSubscription_id(Integer subscription_id) {
this.subscription_id = subscription_id;
}
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
public NewsPaper getNewspaper() {
return newspaper;
}
public void setNewspaper(NewsPaper newspaper) {
this.newspaper = newspaper;
}
public Calendar getDateof() {
return dateof;
}
public void setDateof(Calendar dateof) {
this.dateof = dateof;
}
}
<class name="Employee" dynamic-INSERT="true" dynamic-UPDATE="true" >
<id name="employee_id">
<generator class="native"/>
</id>
<property name="fio"/>
<many-to-one name="department" class="Department" COLUMN="fk_department_id" not-NULL="true"
cascade="save-update"/>
<property name="bio" lazy="true" type="text"/>
<one-to-one name="car" class="Car" cascade="all"/>
<set name="subscriptions" cascade="save-update" inverse="false" TABLE="employee2newspaper" >
<key COLUMN="fk_employee_id" not-NULL="true"/>
<composite-element class="Subscription" >
<parent name="employee" />
<property name="dateof" type="calendar" not-NULL="true"/>
<many-to-one name="newspaper" class="NewsPaper" COLUMN="fk_newspaper_id" not-NULL="true" cascade="save-update"/>
</composite-element>
</set>
</class>
<class name="NewsPaper" dynamic-insert="true" dynamic-update="true">
<id name="newspaper_id">
<generator class="native"/>
</id>
<property name="caption"/>
<set name="subscriptions" cascade="save-update" inverse="true" table="employee2newspaper">
<key column="fk_newspaper_id" not-null="true"/>
<composite-element class="Subscription">
<parent name="newspaper"/>
<property name="dateof" type="calendar" not-null="true"/>
<many-to-one name="employee" class="Employee" column="fk_employee_id" not-null="true" cascade="save-update"/>
</composite-element>
</set>
</class>
CREATE TABLE `employee2newspaper` (
`fk_employee_id` int(11) NOT NULL,
`dateof` datetime DEFAULT NULL,
`fk_newspaper_id` int(11) NOT NULL,
KEY `FKB9DE4FD5632D9988` (`fk_employee_id`),
KEY `FKB9DE4FD55575D22C` (`fk_newspaper_id`),
CONSTRAINT `FKB9DE4FD55575D22C` FOREIGN KEY (`fk_newspaper_id`) REFERENCES `newspaper` (`newspaper_id`),
CONSTRAINT `FKB9DE4FD5632D9988` FOREIGN KEY (`fk_employee_id`) REFERENCES `employee` (`employee_id`)
) ENGINE=InnoDB
Здесь же возникает еще один интересный вопрос о том, как запретить положить в set идентичные пары Читатель-Газета, фактически нам нужно реализовать методы equals & hashCode, но делать это нужно аккуратно т.к. подводных камней хватает.
« Системы управления версиями для программистов и не только. Часть 5 | Тестируй веб-сайты вместе с Badboy. Часть 1 » |