410013796724260
• Webmoney
R335386147728
Z369087728698
Связанные сущности в HibernateСтатья является продолжением описания примера использования Sequence в Hibernate, в которой была рассмотрена только одна сущность и представлено описание сессии Session. В данной статье с использованием практического примера рассмотрим вопрос определения связей между сущностями. Примечание : чтобы не вынуждать Вас обращаться к начальной статье, часть информации здесь будет повторена. Классы в Java могут не только наследоваться друг от друга, но и включать в себя в качестве полей другие классы или коллекции классов. В столбцах таблиц БД нельзя хранить сложные составные типы и коллекции таких типов (за некоторыми исключениями). Это не позволяет сохранять подобный объект в одну таблицу. Но можно сохранять каждый класс в свою собственную таблицу, определив необходимые связи между ними. C описания связей между объектами и начнем. Определение связей между сущностямиДля определения связей между сущностями Hibernate использует аннотации @OneToOne, @OneToMany, @ManyToOne, @ManyToMany. @OneToOneРассмотрим описание аннотации на примере, что каждый гражданин может иметь только один паспорт. И у каждого паспорта может быть только один владелец. Такая связь двух объектов в Hibernate определяется как @OneToOne (один-к-одному). В следующем листинге представлено описание двух сущностей Person (гражданин) и Password (паспорт). Лишние строки, не связанные с аннотацией @OneToOne, не включены в описания сущностей. @Entity @Table (name="users") public class Person { private String name; @OneToOne (optional=false, cascade=CascadeType.ALL) @JoinColumn (name="passport_id") private Passport passport; } @Entity @Table (name="passports") public class Passport { private String series; private String number; @OneToOne (optional=false, mappedBy="passport") private Person owner; } Для связи один к одному в обоих классах к соответствующим полям добавляется аннотация @OneToOne. Параметр optional говорит JPA, является ли значение в этом поле обязательным или нет. Связанное поле в User объявлено с помощью аннотации @JoinColumn, параметр name которой обозначает поле в БД для создания связи. Для того, чтобы объявить сторону, которая не несет ответственности за отношения, используется атрибут mappedBy в сущности Passport. Он ссылается на имя свойства связи (passport) на стороне владельца. Со стороны владельца к аннотации @OneToOne добавляется параметр cascade. В однонаправленных отношениях одна из сторон (и только одна) должна быть владельцем и нести ответственность за обновление связанных полей. В этом случае владельцем выступает сущность User. Каскадирование позволяет указать JPA, что необходимо «сделать со связанным объектом при выполнении операции с владельцем». То есть, когда удаляется Person из базы, JPA самостоятельно определит наличие у него паспорта и удалит вначале паспорт, потом гражданина. Связь в БД между таблицами users и passports осуществляется посредством поля passport_id в таблице users. @OneToMany и @ManyToOneАннотации @OneToMany (один-ко-многим) и @ManyToOne (многие-к-одному) рассмотрим на примере гражданина и его места проживания. Гражданин имеет один основной адрес проживания, но по одному адресу могут проживать несколько человек. В следующем листинге представим эти сущности (лишние поля, не связанные с аннотациями, не отображаются) : @Entity @Table (name="users") public class Person { private String name; @OneToOne (optional=false, cascade=CascadeType.ALL) @JoinColumn (name="passport_id") private Passport passport; @ManyToOne (optional=false, cascade=CascadeType.ALL) @JoinColumn (name="person_id") private Address address; } @Entity public class Address { private String city; private String street; private String building; @OneToMany (mappedBy="address", fetch=FetchType.EAGER) private Collection<Person> tenants; } Владельцем в этом примере также будет класс Person, который имеет поле address, связанное с соответствующим объектом. Поскольку адрес у гражданина только один, то используется аннотация @ManyToOne. Аннотацией @JoinColumn определяется поле связи в таблице БД. Таким образом, параметры этих аннотаций несут такую же смысловую нагрузку, что и у связи @OneToOne. А вот у владеемого объекта на этот раз всё иначе. Поскольку по одному адресу может проживать несколько жильцов, то поле tenants представлено коллекцией, которая имеет аннотацию @OneToMany. Параметр mappedBy также указывает на поле в классе владельца. Параметр fetch=FetchType.EAGER говорит о том, что при загрузке владеемого объекта необходимо сразу загрузить и коллекцию владельцев. Для чтения связанных объектов из БД используются следующие стратегии загрузок (fetch) : EAGER и LAZY. В первом случае объекты коллекции сразу загружаются в память, во втором случае — только при обращении к ним. Оба этих подхода имеют достоинства и недостатки. В случае FetchType.EAGER в памяти будут находиться все загруженные объекты, даже если нужен только один объект из десятка (сотен/тысяч). При использовании данной стратегии необходимо быть внимательным, поскольку при загрузке какого-нибудь корневого объекта, который связан со всеми остальными объектами и коллекциями, можно случайно попытаться загрузить в память и всю базу. Согласно стратегии FetchType.LAZY связанные объекты загружаются только по мере необходимости, т.е. при обращении. Но при этом требуется, чтобы соединение с базой (или транзакция) сохранялись. Если быть точно, то требуется, чтобы объект был attached. Поэтому для работы с lazy объектами тратится больше ресурсов на поддержку соединений. @ManyToManyПримером ассоциации @ManyToMany (многие-ко-многим) могут быть отношения студентов и ВУЗов. В одном институте может быть много студентов, студент может учиться в нескольких ВУЗах. Рассмотрим с начала таблицы БД : create table student ( id integer not null, name varchar(255) default null, CONSTRAINT PK_STUDENT_ID PRIMARY KEY (id) ); create table university ( id integer not null, name varchar(255) default null, CONSTRAINT PK_UNIVERSITY_ID PRIMARY KEY (id) ); create table student_university ( student_id integer not null, university_id integer not null, CONSTRAINT FK_STUDENT_ID FOREIGN KEY (student_id) REFERENCES student (id), CONSTRAINT FK_UNIVERSITY_ID FOREIGN KEY (university_id) REFERENCES university (id) ); Для определения связи @ManyToMany в примере потребуется три таблицы : таблица студентов students, таблица ВУЗов university и таблица связей student_university, в которой будут связаны студенты и ВУЗы. Кроме этого в таблице student_university определены внешние ключи (FOREIGN KEY), предупреждающие появление непрошенных записей при отсутствии родительских. Теперь можно представить описание сущностей : @Entity public class Student { @Id private long id; private String name; @ManyToMany @JoinTable (name="student_university", joinColumns=@JoinColumn (name="student_id"), inverseJoinColumns=@JoinColumn(name="university_id")) private List<University> universities; // set/get не представлены } @Entity public class University { @Id private long id; @Column private String name; @ManyToMany @JoinTable(name="student_university", joinColumns=@JoinColumn(name="university_id"), inverseJoinColumns=@JoinColumn(name="student_id")) private List<Student> students; // set/get не представлены } Список институтов в сущности Student аннотирован с помощью @ManyToMany. Далее следует аннотация @JoinTable, которая определяет таблицу и поля для связи. Параметр name указывает название таблицы (student_university). Параметр joinColumns указывает на поле, которое используется для прямой связи (идентификатор student_id). Параметр inverseJoinColumns указывает на поле, которое используется для обратной связи (идентификатор university_id). Для указания столбцов связи из таблицы используется аннотация @JoinColumn. Сущность университета University описана "зеркально". Пример связанных сущностейРассмотрим пример использования аннотаций @OneToMane и @ManyToOne при определении связанных сущностей. В качестве первой сущности будет использоваться пользователь User. Второй сущностью будет автомобиль Auto. Пользователь может владеть несколькими автомобилями, поэтому сущность User будет связана с Auto связью @OneToMany (один-ко-многим). Сущность Auto будет связана с сущностью User связью @ManyToOne (многие-к-одному). Начнем с объектов базы данных : SQL-скрипты создания таблиц пользователей и автомобилей-- SQL-скрипт создания таблицы пользователей create table USERS ( id Integer not null, login varchar2 (16) null, name varchar2 (64) null, data timestamp default SYSDATE, CONSTRAINT PK_USERID PRIMARY KEY (id) ); -- SQL-скрипт создания таблицы автомобилей CREATE TABLE AUTOS ( aid Integer not null, user_id Integer, name varchar2(32), CONSTRAINT pk_AUTOSK_ID PRIMARY KEY (aid), CONSTRAINT fk_USERID FOREIGN KEY (user_id) REFERENCES USERS (id) ); Записи таблицы пользователей ничего не знают о записях таблицы автомобилей. А записи таблицы Autos связаны с таблицей Users по внешнему ключу (поле user_id). Синтаксис описания внешних ключей в базах данных представлен здесь. SQL-скрипт создания SequenceГенератор последовательностей SEQ_USER используем для определении идентификаторов записей сущностей. Как работать с генераторами последовательностей Sequence в SQL подробно представлено здесь. create sequence SEQ_USER minvalue 1 start with 10 increment by 1 cache 5; Проект тестирования связанных сущностей HibernateНа следующем скриншоте представлена структура проекта hibernate-entities в среде разработки Eclipse. В проекте необходимо определить файл конфигурации hibernate.cfg.xml и классы-сущности (User и Auto). Модуль HibernateExample будет тестировать настройки hibernate и сущностей. Все библиотеки, необходимые для работы с Oracle и hibernate, размещены в поддиректории lib. После включения их в CLASSPATH они отображаются в корне проекта Eclipse. Примечание : в демонстрационном примере hibernate был использован «файл-маппинг» person.cfg.xml сущности Person. В данном примере вместо «файл-маппингов» будем использовать аннотации Подробная информация об аннотациях JPA представлена здесь. Конфигурация hibernateВ конфигурационном XML-файле hibernate.cfg.xml определяем сервер БД (драйвер, пул подключений, диалект, кодировку) и параметры подключения (url, login, password), а также дополнительные параметры, которые будут использованы при работе с сервером. В качестве сервера БД выбран Oracle c пулом подключений в одно соединение. В демонстрационном примере в качестве сервера БД использовался MySQL. Дополнительно определяем истиное значение свойства "show_sql" для отображения в консоли SQL-скриптов, генерируемых библиотекой Hibernate. В заключении в обязательном порядке определяем маппинг сущностей/классов User и Auto, чтобы не вызывать исключений. <?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <!-- Database connection settings --> <property name="hibernate.connection.driver_class"> oracle.jdbc.OracleDriver</property> <property name="hibernate.connection.url"> jdbc:oracle:thin:@localhost:1521:SE</property> <property name="hibernate.connection.username"> scott</property> <property name="hibernate.connection.password"> tiger</property> <!-- JDBC connection pool (use the built-in) --> <property name="hibernate.connection.pool_size">1</property> <!-- Echo all executed SQL to stdout --> <property name="show_sql">true</property> <!-- SQL dialect --> <property name="hibernate.dialect"> org.hibernate.dialect.Oracle10gDialect</property> <property name="hibernate.current_session_context_class"> thread</property> <property name="hibernate.connection.CharSet"> utf8</property> <property name="hibernate.connection.characterEncoding"> utf8</property> <property name="hibernate.connection.useUnicode"> true</property> <!-- Сущности User и Auto--> <mapping class="net.common.model.User"/> <mapping class="net.common.model.Auto"/> </session-factory> </hibernate-configuration> Листинг класса пользователя UserОписание сущности/класса User незначительно изменилось. Добавилось поле List<Auto> autos, определяющее список автомобилей пользователя. @Entity @Table(name = "USERS") public class User { @Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="users_seq") @SequenceGenerator(name="users_seq", sequenceName="SEQ_USER", allocationSize=5) @Column(name="id", updatable=false, nullable=false) private Integer id; @Column (name="name", nullable=true) private String name; @Column (name="login") private String login; @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL) private List<Auto> autos; public User() {} public User(Integer id, String login, String name) { super(); this.id = id; this.login = login; this.name = name; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } @Column public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } @Column (name="name", nullable=true) public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Auto> getAutos() { return autos; } public void setAutos(List<Auto> autos) { this.autos = autos; } public String toString() { String cars = ""; if ((autos != null) && (autos.size() > 0)) { for (int i = 0; i < autos.size(); i++) { if (i > 0) cars += ","; cars += autos.get(i).toString(); } } return "User {id = " + String.valueOf(id) + ", login = '" + login + ", name = '" + name + "', autos =[" + cars + "]}"; } } Описание аннотаций @Table, @Id, @Column, @GeneratedValue, @SequenceGenerator сущности User представлено
в предыдущей статье. Здесь дополним список описанием аннотации
Атрибут cascade обозначает, какие из методов интерфейса Session будут распространяться каскадно к ассоциированным сущностям. Возможные варианты : CascadeType.ALL, CascadeType.PERSIST, CascadeType.MERGE. Необходимо правильно настроить CascadeType, чтобы не подгружать из базы данных лишних ассоциированных объектов-сущностей. Листинг класса автомобиля AutoПри описании поля user используется аннотация @ManyToOne. Аннотация @JoinColumn определяет поле таблицы БД, по которому сущность Auto связана с пользователем User. @Entity @Table(name = "AUTOS") public class Auto { @Id @GeneratedValue (strategy=GenerationType.SEQUENCE, generator="users_seq") @SequenceGenerator (name="users_seq", sequenceName="SEQ_USER", allocationSize=5) @Column (name="aid") private Integer id; @ManyToOne (fetch=FetchType.LAZY, cascade=CascadeType.ALL) @JoinColumn (name="user_id") private User user; @Column(name = "name") private String name; public Auto() { super(); } public Auto(Integer id, User user, String name) { super(); this.id = id; this.user = user; this.name = name; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String toString() { return "Auto {id = " + String.valueOf(id) + ", name = '" + name + ", user_id = " + user.getId() + "}"; } } Листинг класса тестирования HibernateExampleHibernateExample используется для тестирования связей между сущностями Hibernate. Сначала создается сессия в методе createHibernateSession. При создании session устанавливается соединение с БД Oracle. Если сессия создана успешо, то в методе saveUser создаются два объекта (user1, user2), открывается транзакция и объекты сохраняются в БД. Для сохранения объектов используются методы save класса Session. После этого создаются два объекта типа Auto, у которых полям user присваивается значение первого пользователя. Объекты автомобилей сохраняются в БД и транзакция завершается. После сохранения объектов в БД, пользователь user1 обновляется с использованием метода refresh() объекта сессии. Описание методов Session представлено здесь. package net.common; import java.util.List; import java.util.Iterator; import net.common.model.User; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.Transaction; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; import org.hibernate.service.ServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; public class HibernateExample { private Session session = null; //--------------------------------------------------------------- private Session createHibernateSession() { SessionFactory sf = null; ServiceRegistry srvc = null; try { Configuration conf = new Configuration(); conf.configure("hibernate.cfg.xml"); srvc = new StandardServiceRegistryBuilder() .applySettings(conf.getProperties()).build(); sf = conf.buildSessionFactory(srvc); session = sf.openSession(); System.out.println("Создание сессии"); } catch (Throwable e) { throw new ExceptionInInitializerError(e); } return session; } //--------------------------------------------------------------- private void saveUser() { if (session == null) return; User user1 = new User(); user1.setName ("Иван"); user1.setLogin("ivan"); User user2 = new User(); user2.setName ("Сергей"); user2.setLogin("serg" ); Transaction trans = session.beginTransaction(); session.save(user1); session.save(user2); Auto auto = new Auto(); auto.setName("Volvo"); auto.setUser(user1); session.saveOrUpdate(auto); auto = new Auto(); auto.setName("Skoda"); auto.setUser(user1); session.saveOrUpdate(auto); session.flush(); trans.commit(); /* * Обновление detached-объект (выполнение select к БД) * и преобразование его в persistent */ session.refresh(user1); System.out.println("user1 : " + user1.toString()); System.out.println("user2 : " + user2.toString()); } //--------------------------------------------------------------- public HibernateExample() { // Создание сессии session = createHibernateSession(); if (session != null) { System.out.println("session is created"); // Добавление записей в таблицу saveUser(); // Закрытие сессии session.close(); } else { System.out.println("session is not created"); } } //--------------------------------------------------------------- public static void main(String[] args) { new HibernateExample(); System.exit(0); } } Выполнение примераПри выполнении примера в консоль выводится информация, представленная ниже. Поскольку установлен соответствующий флаг в файле конфигурации hibernate.cfg.xml, то формируемые библиотекой Hibernate SQL-скрипты также отображаются в консоли. Информация, выведенная Hibernate в консоль, показывает, что сначала формируются SQL-скрипты (запросы к Sequence) для получения идентификаторов объектов пользователя и автомобиля, после этого создаются SQL-скрипты добавления пользователей и автомобилей в БД. И в заключение Hibernate создает SQL-скрипт select с использованием left outer join для обновления объектов. «Распечатка» описаний пользователей показывет, что первый user имеет автомобили, второй — нет. Как Hibernate с использованием Sequence определяет значения идентификаторов подробно представлено в предыдущей статье. Создание сессии session is created Hibernate: select SEQ_USER.nextval from dual Hibernate: select SEQ_USER.nextval from dual Hibernate: insert into USERS (login, name, id) values (?, ?, ?) Hibernate: insert into USERS (login, name, id) values (?, ?, ?) Hibernate: insert into autos (name, user_id, aid) values (?, ?, ?) Hibernate: insert into autos (name, user_id, aid) values (?, ?, ?) Hibernate: select user0_.id as id1_0_1_, user0_.login as login2_0_1_, user0_.name as name3_0_1_, autos1_.user_id as user_id3_0_3_, autos1_.aid as aid1_1_3_, autos1_.aid as aid1_1_0_, autos1_.name as name2_1_0_, autos1_.user_id as user_id3_1_0_ from USERS user0_ left outer join autos autos1_ on user0_.id=autos1_.user_id where user0_.id=? user1 : User {id = 50, login = 'ivan, name = 'Иван', autos =[Auto {id = 55, name = 'Volvo, user_id = 50}, Auto {id = 56, name = 'Skoda, user_id = 50}]} user2 : User {id = 51, login = 'serg, name = 'Сергей', autos =[]} В продолжении статьи рассмотрен вопрос чтения объектов с фильтрацией и без фильтрации. Удаление связанных сущностейНаличие или отсутствие связанной сущности в базе данных определяет способ удаления. Если связанная сущность отсутствует, то можно использовать оператор DELETE в HQL-запросе объекта Query. Но если сущность содержит связанный объект в таблице БД, то при выполнении транзакции удаления с использованием объекта Query будет вызвано соответствующее исключение. Удаление связанных сущностей необходимо выполнять с использованием объекта сессии Session. Подробнее об этом представлено при описании оператора DELETE в HQL-запросе. Скачать примерИсходный код примера в виде проекта Eclipse hibernate-entities.zip, включающий все необходимые библиотеки hibernate, можно скачать здесь (7.62 Mб). |