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б). |
