Связанные сущности в 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 представлено в предыдущей статье. Здесь дополним список описанием аннотации
@OneToMany
Атрибут fetch в аннотации, определяющий стратегию загрузки дочерних объектов, может принимать одно из двух значений перечисления javax.persistence.FetchType :

  • FetchType.EAGER — загружать коллекцию дочерних объектов вместе с загрузкой родительских объектов;
  • FetchType.LAZY — загружать коллекцию дочерних объектов при первом обращении к ней (вызове метода get) — это так называемая отложенная загрузка.

Атрибут 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() + "}";
    }
}

Листинг класса тестирования HibernateExample

HibernateExample используется для тестирования связей между сущностями 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б).

  Рейтинг@Mail.ru