Статьи

Hibernate Tutorial – ULTIMATE Guide (PDF Download)

ПРИМЕЧАНИЕ РЕДАКЦИИ: В этом посте мы представляем всеобъемлющее руководство по Hibernate. Hibernate ORM (вкратце Hibernate) — это структура объектно-реляционного сопоставления, облегчающая преобразование объектно-ориентированной модели предметной области в традиционную реляционную базу данных. Hibernate решает проблемы несоответствия объектно-реляционного импеданса, заменяя прямые обращения к базе данных, связанные с постоянством, высокоуровневыми функциями обработки объектов.

Hibernate — одна из самых популярных платформ Java. По этой причине мы предоставили множество учебников здесь, на Java Code Geeks, большинство из которых можно найти здесь .

Теперь мы хотели создать отдельную справочную статью, которая предоставит платформу для работы с Hibernate и поможет вам быстро запустить приложения Hibernate. Наслаждайтесь!

Вступление

Hibernate является одной из самых популярных инфраструктур объектно-реляционного сопоставления (ORM) в мире Java. Это позволяет разработчикам отображать структуры объектов обычных классов Java в реляционную структуру базы данных. С помощью инфраструктуры ORM работа по сохранению данных из экземпляров объектов в памяти в постоянное хранилище данных и их загрузке обратно в ту же структуру объектов становится значительно проще.

В то же время решения ORM, такие как Hibernate, стремятся абстрагироваться от конкретного продукта, используемого для хранения данных. Это позволяет использовать один и тот же код Java с различными продуктами баз данных без необходимости писать код, который обрабатывает тонкие различия между поддерживаемыми продуктами.

Hibernate также является провайдером JPA, что означает, что он реализует API персистентности Java (JPA) . JPA является независимой от производителя спецификацией для отображения объектов Java в таблицы реляционных баз данных. Поскольку другая статья серии Ultimate уже посвящена JPA, эта статья посвящена Hibernate и поэтому использует не аннотации JPA, а файлы конфигурации, специфичные для Hibernate.

Hibernate состоит из трех различных компонентов:

  • Объекты : классы, которые отображаются в Hibernate на таблицы системы реляционных баз данных, являются простыми классами Java (Plain Old Java Objects).
  • Объектно-реляционные метаданные : информация о том, как отобразить объекты в реляционную базу данных, предоставляется либо аннотациями (начиная с Java 1.5), либо устаревшими файлами конфигурации на основе XML. Информация в этих файлах используется во время выполнения для отображения в хранилище данных и обратно в объекты Java.
  • Язык запросов Hibernate (HQL) : при использовании Hibernate запросы, отправляемые в базу данных, не нужно формулировать в собственном SQL, но их можно указать с помощью языка запросов Hibernate. Поскольку эти запросы переводятся во время выполнения на используемый в настоящее время диалект выбранного продукта, запросы, сформулированные на HQL, не зависят от диалекта SQL конкретного поставщика.

В этом руководстве мы рассмотрим различные аспекты инфраструктуры и разработаем простое приложение Java SE, которое сохраняет и извлекает данные в / из реляционной базы данных. Мы будем использовать следующие библиотеки / среды:

  • maven> = 3.0 в качестве среды сборки
  • Спящий режим (4.3.8.Final)
  • H2 как реляционная база данных (1.3.176)

Настройка проекта

В качестве первого шага мы создадим простой проект maven в командной строке:

1
mvn archetype:create -DgroupId=com.javacodegeeks.ultimate -DartifactId=hibernate

Эта команда создаст следующую структуру в файловой системе:

01
02
03
04
05
06
07
08
09
10
11
12
|-- src
|   |-- main
|   |   `-- java
|   |       `-- com
|   |           `-- javacodegeeks
|   |                `-- ultimate
|   `-- test
|   |   `-- java
|   |       `-- com
|   |           `-- javacodegeeks
|   |                `-- ultimate
`-- pom.xml

Библиотеки, от которых зависит наша реализация, добавляются в раздел зависимостей файла pom.xml следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
1.3.176
4.3.8.Final
 
 
 
 
    com.h2database
    h2
    ${h2.version}
 
 
    org.hibernate
    hibernate-core
    ${hibernate.version}

Чтобы получить лучший обзор отдельных версий, мы определяем каждую версию как свойство maven и ссылаемся на нее позже в разделе зависимостей.

3. Основы

3.1. SessionFactory и Session

Теперь мы можем приступить к реализации нашего первого отображения O / R. Давайте начнем с простого класса, который предоставляет метод run() который вызывается в main методе приложения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Main {
    private static final Logger LOGGER = Logger.getLogger("Hibernate-Tutorial");
 
    public static void main(String[] args) {
        Main main = new Main();
        main.run();
    }
 
    public void run() {
        SessionFactory sessionFactory = null;
        Session session = null;
        try {
            Configuration configuration = new Configuration();
            configuration.configure("hibernate.cfg.xml");
            ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties()).build();
            sessionFactory = configuration.buildSessionFactory(serviceRegistry);
            session = sessionFactory.openSession();
            persistPerson(session);
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } finally {
            if (session != null) {
                session.close();
            }
            if (sessionFactory != null) {
                sessionFactory.close();
            }
        }
    }
    ...

Метод run() создает новый экземпляр класса org.hibernate.cfg.Configuration который впоследствии настраивается с использованием XML-файла hibernate.cfg.xml . Поместив файл конфигурации в папку src/main/resources нашего проекта, maven поместит его в корень созданного файла jar. Таким образом, файл находится во время выполнения на пути к классам.

На втором этапе метод run() создает ServiceRegistry которое использует ранее загруженную конфигурацию. Экземпляр этого ServiceRegistry теперь можно передать в качестве аргумента методу buildSessionFactroy() Configuration . Теперь этот SessionFactory можно использовать для получения сеанса, необходимого для хранения и загрузки объектов в основное хранилище данных.

Файл конфигурации hibernate.cfg.xml имеет следующее содержимое:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
org.h2.Driver
jdbc:h2:~/hibernate;AUTOCOMMIT=OFF
 
 
1
org.hibernate.dialect.H2Dialect
thread
org.hibernate.cache.internal.NoCacheProvider
true
true
create

Как видно из приведенного выше примера, файл конфигурации определяет набор свойств для фабрики сеансов. Первое свойство connection.driver_class указывает драйвер базы данных, который следует использовать. В нашем примере это драйвер для базы данных H2. Через свойство connection.url указывается JDBC-URL. В нашем случае определяет, что мы хотим использовать h2 и что единственный файл базы данных, в котором H2 хранит свои данные, должен находиться в домашнем каталоге пользователя и иметь имя hibernate ( ~/hibernate ). Поскольку мы хотим зафиксировать наши транзакции в примере кода самостоятельно, мы также определяем особую конфигурационную опцию H2 AUTOCOMMIT=OFF .

Затем файл конфигурации определяет имя пользователя и пароль для соединения с базой данных, а также размер пула соединений. Наше приложение-пример просто выполняет код в одном потоке, поэтому мы можем установить размер пула в один. В случае приложения, которое имеет дело с несколькими потоками и пользователями, должен быть выбран соответствующий размер пула.

Свойство dialect указывает класс Java, который выполняет перевод в специфический для базы данных диалект SQL.

Начиная с версии 3.1, Hibernate предоставляет метод с именем SessionFactory.getCurrentSession() который позволяет разработчику получить ссылку на текущий сеанс. С помощью свойства конфигурации current_session_context_class можно настроить, откуда Hibernate должен получать этот сеанс. Значением по умолчанию для этого свойства является jta означающее, что Hibernate получает сеанс из базового API транзакций Java (JTA). Поскольку в этом примере мы не используем JTA, мы указываем Hibernate с thread значений конфигурации сохранять и извлекать сеанс в / из текущего потока.

Для простоты мы не хотим использовать кеш сущностей. Поэтому мы устанавливаем для свойства cache.provider_class значение org.hibernate.cache.internal.NoCacheProvider .

Следующие два параметра указывают Hibernate распечатывать каждый оператор SQL на консоль и форматировать его для лучшей читаемости. Чтобы освободить нас для целей разработки от необходимости создавать схему вручную, мы даем Hibernate с параметром hbm2ddl.auto установленным для create чтобы создавать все таблицы во время запуска.

И последнее, но не менее важное: мы определяем файл ресурсов сопоставления, который содержит всю информацию сопоставления для нашего приложения. Содержание этого файла будет объяснено в следующих разделах.

Как упоминалось выше, сеанс используется для связи с хранилищем данных и фактически представляет собой соединение JDBC. Это означает, что все взаимодействие с соединением осуществляется через сеанс. Он однопоточный и предоставляет кеш для всех объектов, с которыми он до сих пор работал. Поэтому каждый поток в приложении должен работать со своим собственным сеансом, который он получает из фабрики сеансов.

В отличие от сеанса, фабрика сеансов является поточно-ориентированной и обеспечивает неизменный кеш для отображений определений. Для каждой базы данных существует ровно одна фабрика сессий. Необязательно, фабрика сеансов может предоставить в дополнение к кэшу первого уровня сеанса кэш второго уровня приложения.

3.2. операции

В файле конфигурации hibernate.cfg.xml мы настроили управление транзакциями самостоятельно. Следовательно, мы должны вручную запускать и фиксировать или откатывать каждую транзакцию. Следующий код демонстрирует, как получить новую транзакцию из сеанса и как запустить и зафиксировать ее:

01
02
03
04
05
06
07
08
09
10
11
try {
    Transaction transaction = session.getTransaction();
    transaction.begin();
    ...
    transaction.commit();
} catch (Exception e) {
    if (session.getTransaction().isActive()) {
        session.getTransaction().rollback();
    }
    throw e;
}

На первом этапе мы вызываем getTransaction() , чтобы получить ссылку для новой транзакции. Эта транзакция немедленно начинается с вызова метода begin() . Если следующий код выполняется без каких-либо исключений, транзакция фиксируется. В случае возникновения исключения и текущей транзакции транзакция откатывается.

Поскольку приведенный выше код одинаков для всех последующих примеров, он не повторяется в точной форме снова и снова. Шаги по преобразованию кода в форму многократного использования, используя, например, шаблон шаблона, оставлены для читателя.

3.3. таблицы

Теперь, когда мы узнали о фабриках сессий, сессиях и транзакциях, пришло время начать с отображения первого класса. Чтобы легко начать, мы выбираем простой класс с несколькими простыми атрибутами:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Person {
    private Long id;
    private String firstName;
    private String lastName;
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getFirstName() {
        return firstName;
    }
 
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
 
    public String getLastName() {
        return lastName;
    }
 
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

Класс Person поставляется с двумя атрибутами для хранения имени человека ( firstName и lastName ). Поле id используется для хранения уникального идентификатора объекта в виде длинного значения. В этом уроке мы будем использовать файлы сопоставления вместо аннотаций, поэтому мы указываем сопоставление этого класса с таблицей T_PERSON следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
 
<hibernate-mapping package="hibernate.entity">
     <class name="Person" table="T_PERSON">
        <id name="id" column="ID">
            <generator class="native"/>
        </id>
        <property name="firstName" column="FIRST_NAME"/>
        <property name="lastName" column="LAST_NAME"/>
      </class>
</hibernate-mapping>

Элемент XML hibernate-mapping используется для определения пакета, в котором находятся наши сущности (здесь: hibernate.entity ). Внутри этого элемента предусмотрен один элемент class для каждого класса, который должен быть сопоставлен с таблицей в базе данных.

Элемент id указывает имя ( name ) поля класса, которое содержит уникальный идентификатор, и имя столбца, в котором хранится это значение ( ID ). Благодаря своему generator дочерних элементов Hibernate узнает, как создать уникальный идентификатор для каждой сущности. Помимо значения, показанного выше, Hibernate поддерживает длинный список различных стратегий.

native стратегия просто выбирает лучшую стратегию для используемого продукта базы данных. Следовательно, эта стратегия может применяться для различных продуктов. Другими возможными значениями являются, например: sequence (использует последовательность в базе данных), uuid (генерирует 128-битный UUID) и assigned (позволяет приложению присваивать значение самостоятельно). Помимо предопределенных стратегий, можно реализовать собственную стратегию, реализовав интерфейс org.hibernate.id.IdentifierGenerator .

Поля firstName и lastName сопоставляются со столбцами FIRST_NAME и LAST_NAME с помощью property элемента XML. Атрибуты name и column определяют имя поля в классе и столбце соответственно.

Следующий код показывает пример того, как хранить человека в базе данных:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
private void persistPerson(Session session) throws Exception {
    try {
        Transaction transaction = session.getTransaction();
        transaction.begin();
        Person person = new Person();
        person.setFirstName("Homer");
        person.setLastName("Simpson");
        session.save(person);
        transaction.commit();
    } catch (Exception e) {
        if (session.getTransaction().isActive()) {
            session.getTransaction().rollback();
        }
        throw e;
    }
}

Рядом с кодом для обработки транзакции он создает новый экземпляр класса Person и присваивает два значения полям firstName и lastName . Наконец, он сохраняет человека в базе данных, вызывая метод save() сеанса.

Когда мы выполняем приведенный выше код, на консоли выводятся следующие операторы SQL:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
Hibernate:
    drop table T_PERSON if exists
Hibernate:
    create table T_PERSON (
        ID bigint generated by default as identity,
        FIRST_NAME varchar(255),
        LAST_NAME varchar(255),
        primary key (ID)
    )
Hibernate:
    insert
    into
        T_PERSON
        (ID, firstName, lastName, ID_ID_CARD)
    values
        (null, ?, ?, ?)

Поскольку мы решили позволить Hibernate удалять и создавать таблицы при запуске, первые распечатанные операторы — это drop table и операторы create table . Мы также можем увидеть три столбца ID , FIRST_NAME и LAST_NAME таблицы T_PERSON а также определение первичного ключа (здесь: ID ).

После того, как таблица была создана, вызов session.save() выдает оператор insert в базу данных. Поскольку Hibernate внутренне использует PreparedStatement , мы не видим значений на консоли. Если вы также хотите увидеть значения, которые связаны с параметрами PreparedStatement , вы можете установить уровень ведения журнала для регистратора org.hibernate.type как FINEST . Это делается в файле с именем logging.properties следующего содержания (путь к файлу можно -Djava.util.logging.config.file=src/main/resources/logging.properties например, в виде системного свойства -Djava.util.logging.config.file=src/main/resources/logging.properties ) :

1
2
3
4
5
6
7
8
.handlers = java.util.logging.ConsoleHandler
.level = INFO
 
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
 
org.hibernate.SQL.level = FINEST
org.hibernate.type.level = FINEST

Установка регистратора org.hibernate.SQL имеет тот же эффект, что и установка для свойства show_sql в файле конфигурации Hibernate значения true .

Теперь вы можете увидеть следующие выходные данные и фактические значения на консоли:

01
02
03
04
05
06
07
08
09
10
DEBUG:
    insert
    into
        T_PERSON
        (ID, FIRST_NAME, LAST_NAME, ID_ID_CARD)
    values
        (null, ?, ?, ?)
TRACE: binding parameter [1] as [VARCHAR] - [Homer]
TRACE: binding parameter [2] as [VARCHAR] - [Simpson]
TRACE: binding parameter [3] as [BIGINT] - [null]

4. Наследование

Интересной особенностью таких решений отображения O / R, как Hibernate, является использование наследования. Пользователь может выбрать способ сопоставления суперкласса и подкласса с таблицами реляционной базы данных. Hibernate поддерживает следующие стратегии отображения:

  • Одна таблица на класс : и суперкласс, и подкласс отображаются в одну и ту же таблицу. В дополнительном столбце указано, является ли строка экземпляром суперкласса или подкласса, а поля, которых нет в суперклассе, оставлены пустыми.
  • Объединенный подкласс : эта стратегия использует отдельную таблицу для каждого класса, тогда как таблица для подкласса хранит только те поля, которые отсутствуют в суперклассе. Чтобы получить все значения для экземпляра подкласса, необходимо выполнить соединение между двумя таблицами.
  • Таблица для класса : эта стратегия также использует отдельную таблицу для каждого класса, но хранит в таблице для подкласса также поля суперкласса. При этой стратегии одна строка в таблице подклассов содержит все значения, и для извлечения всех значений не требуется оператор соединения.

Подход, который мы собираемся исследовать, — это подход «Одна таблица на класс». В качестве подкласса человека мы выбираем класс Geek :

01
02
03
04
05
06
07
08
09
10
11
public class Geek extends Person {
    private String favouriteProgrammingLanguage;
 
    public String getFavouriteProgrammingLanguage() {
            return favouriteProgrammingLanguage;
    }
 
    public void setFavouriteProgrammingLanguage(String favouriteProgrammingLanguage) {
        this.favouriteProgrammingLanguage = favouriteProgrammingLanguage;
    }
}

Класс расширяет уже известный класс Person и добавляет дополнительное поле с именем favouriteProgrammingLanguage . Файл сопоставления для этого варианта использования выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
 
<hibernate-mapping package="hibernate.entity">
   <class name="Person" table="T_PERSON">
       <id name="id" column="ID">
           <generator class="native"/>
        </id>
        <discriminator column="PERSON_TYPE" type="string"/>
        <property name="firstName" column="FIRST_NAME"/>
        <property name="lastName" column="LAST_NAME"/>
        <subclass name="Geek" extends="Person">
             <property name="favouriteProgrammingLanguage" column="FAV_PROG_LANG"/>
         </subclass>
   </class>
</hibernate-mapping>

Первым отличием является введение столбца discriminator . Как упоминалось выше, в этом столбце хранится информация о типе текущего экземпляра. В нашем случае мы называем это PERSON_TYPE и позволяем для лучшей читаемости строки обозначать фактический тип. По умолчанию Hibernate принимает только имя класса в этом случае. Для экономии места можно также использовать столбец типа integer.

Помимо дискриминатора мы также добавили элемент subclass который информирует Hibernate о новом классе Java Geek и его поле favouriteProgrammingLanguage которое должно быть сопоставлено со столбцом FAV_PROG_LANG .

В следующем примере кода показано, как хранить экземпляры типа Geek в базе данных:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
session.getTransaction().begin();
Geek geek = new Geek();
geek.setFirstName("Gavin");
geek.setLastName("Coffee");
geek.setFavouriteProgrammingLanguage("Java");
session.save(geek);
geek = new Geek();
geek.setFirstName("Thomas");
geek.setLastName("Micro");
geek.setFavouriteProgrammingLanguage("C#");
session.save(geek);
geek = new Geek();
geek.setFirstName("Christian");
geek.setLastName("Cup");
geek.setFavouriteProgrammingLanguage("Java");
session.save(geek);
session.getTransaction().commit();

Выполнение кода, показанного выше, приводит к следующему выводу:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
Hibernate:
    drop table T_PERSON if exists
Hibernate:
    create table T_PERSON (
        ID bigint generated by default as identity,
        PERSON_TYPE varchar(255) not null,
        FIRST_NAME varchar(255),
        LAST_NAME varchar(255),
        FAV_PROG_LANG varchar(255),
        primary key (ID)
    )
Hibernate:
    insert
    into
        T_PERSON
        (ID, FIRST_NAME, LAST_NAME, FAV_PROG_LANG, PERSON_TYPE)
    values
        (null, ?, ?, ?, 'hibernate.entity.Geek')

В отличие от предыдущего примера, таблица T_PERSON теперь содержит два новых столбца PERSON_TYPE и FAV_PROG_LANG . Столбец PERSON_TYPE содержит значение hibernate.entity.Geek для гиков.

Чтобы изучить содержимое таблицы T_PERSON , мы можем использовать приложение Shell, поставляемое в файле jar H2:

1
2
3
4
5
6
7
8
> java -cp h2-1.3.176.jar org.h2.tools.Shell -url jdbc:h2:~/hibernate
...
sql> select * from t_person;
ID | PERSON_TYPE             | FIRST_NAME | LAST_NAME | FAV_PROG_LANG
1  | hibernate.entity.Person | Homer      | Simpson   | null
2  | hibernate.entity.Geek   | Gavin      | Coffee    | Java
3  | hibernate.entity.Geek   | Thomas     | Micro     | C#
4  | hibernate.entity.Geek   | Christian  | Cup       | Java

Как обсуждалось выше, столбец PERSON_TYPE хранит тип экземпляра, тогда как столбец FAV_PROG_LANG содержит значение null для экземпляров суперкласса Person .

Изменив определение отображения таким образом, чтобы оно выглядело следующим образом, Hibernate создаст для суперкласса и подкласса отдельную таблицу:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
 
<hibernate-mapping package="hibernate.entity">
    <class name="Person" table="T_PERSON">
        <id name="id" column="ID">
            <generator class="native"/>
        </id>
        <property name="firstName" column="FIRST_NAME"/>
        <property name="lastName" column="LAST_NAME"/>
        <joined-subclass name="Geek" table="T_GEEK">
             <key column="ID_PERSON"/>
              <property name="favouriteProgrammingLanguage" column="FAV_PROG_LANG"/>
         </joined-subclass>
     </class>
</hibernate-mapping>

Элемент XML T_GEEK сообщает Hibernate о создании таблицы T_GEEK для подкласса Geek с дополнительным столбцом ID_PERSON . Этот столбец дополнительного ключа хранит внешний ключ к таблице T_PERSON , чтобы назначить каждую строку в T_GEEK его родительской строке в T_PERSON .

Использование приведенного выше кода Java для хранения нескольких гиков в базе данных приводит к следующему выводу на консоль:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
Hibernate:
    drop table T_GEEK if exists
Hibernate:
    drop table T_PERSON if exists
Hibernate:
    create table T_GEEK (
        ID_PERSON bigint not null,
        FAV_PROG_LANG varchar(255),
        primary key (ID_PERSON)
    )
Hibernate:
    create table T_PERSON (
        ID bigint generated by default as identity,
        FIRST_NAME varchar(255),
        LAST_NAME varchar(255),
        primary key (ID)
    )
Hibernate:
    alter table T_GEEK
        add constraint FK_p2ile8qooftvytnxnqtjkrbsa
        foreign key (ID_PERSON)
        references T_PERSON

Теперь Hibernate создает две таблицы вместо одной и определяет внешний ключ для таблицы T_GEEK которая ссылается на таблицу T_PERSON . Таблица T_GEEK состоит из двух столбцов: ID_PERSON для ссылки на соответствующего человека и FAV_PROG_LANG для хранения любимого языка программирования.

Хранение гика в базе данных теперь состоит из двух операторов вставки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
Hibernate:
    insert
    into
        T_PERSON
        (ID, FIRST_NAME, LAST_NAME, ID_ID_CARD)
    values
        (null, ?, ?, ?)
Hibernate:
    insert
    into
        T_GEEK
        (FAV_PROG_LANG, ID_PERSON)
    values
        (?, ?)

Первый оператор вставляет новую строку в таблицу T_PERSON , а второй — новую строку в таблицу T_GEEK . Содержимое этих двух таблиц выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
sql> select * from t_person;
ID | FIRST_NAME | LAST_NAME
1  | Homer      | Simpson 
2  | Gavin      | Coffee  
3  | Thomas     | Micro   
4  | Christian  | Cup     
 
sql> select * from t_geek;
ID_PERSON | FAV_PROG_LANG
2         | Java
3         | C#
4         | Java

Очевидно, что таблица T_PERSON хранит только атрибуты суперкласса, тогда как таблица T_GEEK хранит только значения полей для подкласса. Столбец ID_PERSON ссылается на соответствующую строку из родительской таблицы.

Следующая исследуемая стратегия — «таблица на класс». Подобно последней стратегии, эта также создает отдельную таблицу для каждого класса, но, напротив, таблица для подкласса содержит также все столбцы суперкласса. При этом одна строка в такой таблице содержит все значения для создания экземпляра этого типа без необходимости присоединения дополнительных данных из родительской таблицы. На огромном наборе данных это может повысить производительность запросов, поскольку объединениям необходимо дополнительно найти соответствующие строки в родительской таблице. Этот дополнительный поиск требует времени, которое обходится при таком подходе.

Чтобы использовать эту стратегию для описанного выше варианта использования, файл сопоставления можно переписать, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
 
<hibernate-mapping package="hibernate.entity">
     <class name="Person" table="T_PERSON">
         <id name="id" column="ID">
             <generator class="sequence"/>
         </id>
         <property name="firstName" column="FIRST_NAME"/>
         <property name="lastName" column="LAST_NAME"/>
         <union-subclass name="Geek" table="T_GEEK">
              <property name="favouriteProgrammingLanguage" column="FAV_PROG_LANG"/>
         </union-subclass>
 
      </class>
</hibernate-mapping>

XML-элемент union-subclass предоставляет имя объекта ( Geek ), а также имя отдельной таблицы ( T_GEEK ) в качестве атрибутов. Как и в других подходах, поле favouriteProgrammingLanguage объявляется как свойство подкласса.

Другое важное изменение в отношении других подходов содержится в строке, которая определяет генератор идентификаторов. Поскольку другие подходы используют native генератор, который возвращается на H2 к столбцу идентификаторов, этот подход требует генератора идентификаторов, который создает идентификаторы, которые являются уникальными для обеих таблиц ( T_PERSON и T_GEEK ).

Столбец идентификации — это просто специальный тип столбца, который автоматически создает для каждой строки новый идентификатор. Но с двумя таблицами у нас также есть два столбца идентификаторов, и T_PERSON идентификаторы в таблице T_PERSON могут быть такими же, как в таблице T_GEEK . Это противоречит требованию, что сущность типа Geek может быть создана только путем чтения одной строки таблицы T_GEEK и что идентификаторы для всех людей и вундеркиндов являются уникальными. Поэтому мы используем последовательность вместо столбца идентификаторов, переключая значение атрибута class с native на sequence .

Теперь операторы DDL, созданные Hibernate, выглядят следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Hibernate:
    drop table T_GEEK if exists
Hibernate:
    drop table T_PERSON if exists
Hibernate:
    drop sequence if exists hibernate_sequence
Hibernate:
    create table T_GEEK (
        ID bigint not null,
        FIRST_NAME varchar(255),
        LAST_NAME varchar(255),
        FAV_PROG_LANG varchar(255),
        primary key (ID)
    )
Hibernate:
    create table T_PERSON (
        ID bigint not null,
        FIRST_NAME varchar(255),
        LAST_NAME varchar(255),
        primary key (ID)
    )
Hibernate:
    create sequence hibernate_sequence

Вывод выше ясно показывает, что таблица T_GEEK теперь содержит рядом с FAV_PROG_LANG также столбцы для суперкласса ( FIRST_NAME и LAST_NAME ). Операторы не создают внешний ключ между двумя таблицами. Также обратите внимание, что теперь ID столбца больше не является столбцом идентификаторов, а вместо этого создается последовательность.

Вставка человека и выродка выдает следующие утверждения в базу данных:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
Hibernate:
    call next value for hibernate_sequence
Hibernate:
    insert
    into
        T_PERSON
        (FIRST_NAME, LAST_NAME, ID)
    values
        (?, ?, ?, ?)
Hibernate:
    call next value for hibernate_sequence
Hibernate:
    insert
    into
        T_GEEK
        (FIRST_NAME, LAST_NAME, FAV_PROG_LANG, ID)
    values
        (?, ?, ?, ?, ?)

Для одного человека и одного гика у нас, очевидно, только два оператора вставки. Таблица T_GEEK полностью заполнена одной вставкой и содержит все значения экземпляра Geek :

1
2
3
4
5
6
7
8
9
sql> select * from t_person;
ID | FIRST_NAME | LAST_NAME
1  | Homer      | Simpson  
 
sql> select * from t_geek;
ID | FIRST_NAME | LAST_NAME | FAV_PROG_LANG
3  | Gavin      | Coffee    | Java
4  | Thomas     | Micro     | C#
5  | Christian  | Cup       | Java

5. Отношения

До сих пор мы видели единственную связь между двумя таблицами — «расширяет». Помимо простого наследования, Hibernate также может отображать отношения, основанные на списках, в которых один объект имеет список экземпляров другого объекта. Различают следующие типы отношений:

  • Один к одному : это обозначает простое отношение, в котором один объект типа A принадлежит точно одному объекту типа B.
  • Много к одному : как видно из названия, это отношение охватывает случай, когда сущность типа A имеет много дочерних сущностей типа B.
  • Много ко многим : в этом случае может быть много объектов типа A, которые принадлежат многим объектам типа B.

Чтобы немного лучше понять эти различные типы отношений, мы рассмотрим их в следующем.

5.1. Один к одному

В качестве примера для случая «один к одному» мы добавляем следующий класс в нашу модель сущности:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class IdCard {
    private Long id;
    private String idNumber;
    private Date issueDate;
    private boolean valid;
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getIdNumber() {
        return idNumber;
    }
 
    public void setIdNumber(String idNumber) {
        this.idNumber = idNumber;
    }
 
    public Date getIssueDate() {
        return issueDate;
    }
 
    public void setIssueDate(Date issueDate) {
        this.issueDate = issueDate;
    }
 
    public boolean isValid() {
        return valid;
    }
 
    public void setValid(boolean valid) {
        this.valid = valid;
    }
}

Удостоверение личности как внутренний уникальный идентификатор, а также внешний idNumber, дата выпуска и логический флаг, который указывает, действительна ли карта или нет.

С другой стороны отношения человек получает новое поле с именем idCard которое ссылается на карту этого человека:

01
02
03
04
05
06
07
08
09
10
11
12
13
public class Person {
    ...
    private IdCard idCard;
     
    ...
     
    public IdCard getIdCard() {
        return idCard;
    }
 
    public void setIdCard(IdCard idCard) {
        this.idCard = idCard;
    }

Чтобы отобразить это отношение с помощью специального файла отображения Hibernate, мы изменим его следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<hibernate-mapping package="hibernate.entity">
     <class name="IdCard" table="T_ID_CARD">
         <id name="id" column="ID">
             <generator class="sequence"/>
         </id>
      </class>
      <class name="Person" table="T_PERSON">
          <id name="id" column="ID">
              <generator class="sequence"/>
          </id>
          <property name="firstName" column="FIRST_NAME"/>
          <property name="lastName" column="LAST_NAME"/>
          <many-to-one name="idCard" column="ID_ID_CARD" unique="true"/>
          <union-subclass name="Geek" table="T_GEEK">
               <property name="favouriteProgrammingLanguage" column="FAV_PROG_LANG"/>
          </union-subclass>
        </class>
</hibernate-mapping>

Прежде всего мы добавляем новый элемент class для нового класса, указывая имя класса и имя соответствующей ему таблицы (здесь: T_ID_CARD ). Поле id становится уникальным идентификатором и должно заполняться значением последовательности.

С другой стороны, отображение Person теперь содержит новый XML-элемент many-to-one и ссылается своим name атрибута на поле класса Person котором хранится ссылка на IdCard . column необязательного атрибута позволяет нам указать точное имя столбца внешнего ключа в таблице T_PERSON которая связана с удостоверением личности человека. Поскольку это отношение должно быть типа «один к одному», мы должны установить атрибут, unique для true .

Выполнение этой конфигурации приводит к следующим инструкциям DDL (обратите внимание, что для уменьшения количества таблиц мы вернулись к подходу «одна таблица на класс», где у нас есть только одна таблица для суперкласса и подкласса):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Hibernate:
    drop table T_ID_CARD if exists
Hibernate:
    drop table T_PERSON if exists
Hibernate:
    drop sequence if exists hibernate_sequence
Hibernate:
    create table T_ID_CARD (
        ID bigint not null,
        ID_NUMBER varchar(255),
        ISSUE_DATE timestamp,
        VALID boolean,
        primary key (ID)
    )
Hibernate:
    create table T_PERSON (
        ID bigint not null,
        PERSON_TYPE varchar(255) not null,
        FIRST_NAME varchar(255),
        LAST_NAME varchar(255),
        ID_ID_CARD bigint,
        FAV_PROG_LANG varchar(255),
        primary key (ID)
    )
Hibernate:
    alter table T_PERSON
        add constraint UK_96axqtck4kc0be4ancejxtu0p  unique (ID_ID_CARD)
Hibernate:
    alter table T_PERSON
        add constraint FK_96axqtck4kc0be4ancejxtu0p
        foreign key (ID_ID_CARD)
        references T_ID_CARD
Hibernate:
    create sequence hibernate_sequence

Что изменилось в отношении предыдущих примеров, так это то, что таблица T_PERSON теперь содержит дополнительный столбец ID_ID_CARD который определен как внешний ключ к таблице T_ID_CARD . Сама таблица T_ID_CARD , как и ожидалось, содержит три столбца ID_NUMBER , ISSUE_DATE и ISSUE_DATE .

Код Java для вставки человека вместе с его удостоверением личности выглядит следующим образом:

1
2
3
4
5
6
7
8
9
Person person = new Person();
person.setFirstName("Homer");
person.setLastName("Simpson");
session.save(person);
IdCard idCard = new IdCard();
idCard.setIdNumber("4711");
idCard.setIssueDate(new Date());
person.setIdCard(idCard);
session.save(idCard);

Создание экземпляра IdCard вызывает IdCard , также обратите внимание, что ссылка Person на IdCard в последней строке, кроме одной. Оба экземпляра передаются в метод save() Hibernate.

Рассматривая приведенный выше код более подробно, можно поспорить, почему мы должны передавать оба экземпляра в метод save() сеанса. Это оправдано, поскольку Hibernate позволяет определить, что определенная операция должна быть «каскадной» при обработке полного графа сущностей. Чтобы включить каскадирование для связи с IdCard мы можем просто добавить cascade атрибутов к элементу « many-to-one в файле отображения:

1
<many-to-one name="idCard" column="ID_ID_CARD" unique="true" cascade="all"/>

Использование значения all указывает Hibernate каскадировать все типы операций. Поскольку это не всегда предпочтительный способ обработки отношений между сущностями, можно также выбрать только определенные операции:

1
2
<many-to-one name="idCard" column="ID_ID_CARD" unique="true" cascade="save-update,refresh"/ ?-
>

Приведенный выше пример демонстрирует, как настроить сопоставление таким образом, чтобы saveOrUpdate() только вызовы save() , saveOrUpdate() и refresh (перечитывает состояние данного объекта из базы данных). Вызовы методов Hibernate delete() или lock() , например, не будут перенаправлены.

Используя одну из двух приведенных выше конфигураций, код для сохранения человека вместе с его удостоверением личности можно переписать в следующую:

1
2
3
4
5
6
7
8
Person person = new Person();
person.setFirstName("Homer");
person.setLastName("Simpson");
IdCard idCard = new IdCard();
idCard.setIdNumber("4711");
idCard.setIssueDate(new Date());
person.setIdCard(idCard);
session.save(person);

Вместо использования метода save() можно также использовать в этом случае метод saveOrUpdate() . Цель метода saveOrUpdate() заключается в том, что его также можно использовать для обновления существующего объекта. Тонкое различие между обеими реализациями заключается в том, что методы save() возвращают созданный идентификатор новой сущности:

1
Long personId = (Long) session.save(person);

Это полезно при написании, например, кода на стороне сервера, который должен возвращать этот идентификатор вызывающей стороне метода. С другой стороны, метод update() не возвращает идентификатор, поскольку предполагает, что объект уже был сохранен в хранилище данных и, следовательно, должен иметь идентификатор. Попытка обновить сущность без идентификатора вызовет исключение:

1
org.hibernate.TransientObjectException: The given object has a null identifier: ...

Поэтому saveOrUpdate() помогает в тех случаях, когда нужно пропустить код, который решает, была ли сущность уже сохранена или нет.

5.2. Один ко многим

Другое отношение, которое часто появляется во время O / R-отображений, это отношение «один ко многим». В этом случае набор объектов принадлежит одному объекту другого типа. Чтобы смоделировать такое отношение, мы добавляем класс Phone в нашу модель:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Phone {
    private Long id;
    private String number;
    private Person person;
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getNumber() {
        return number;
    }
 
    public void setNumber(String number) {
        this.number = number;
    }
 
    public Person getPerson() {
        return person;
    }
 
    public void setPerson(Person person) {
        this.person = person;
    }
}

Как обычно, сущность Phone имеет внутренний идентификатор ( id ) и поле для хранения фактического номера телефона. Полевой person сохраняет ссылку обратно на человека, которому принадлежит этот телефон. Поскольку у одного человека может быть несколько телефонов, мы добавляем класс Set в Person который собирает все телефоны одного человека:

01
02
03
04
05
06
07
08
09
10
11
12
public class Person {
    ...
    private Set phones = new HashSet();
    ...
    public Set getPhones() {
        return phones;
    }
 
    public void setPhones(Set phones) {
        this.phones = phones;
    }
}

Файл отображения должен быть обновлен соответственно:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
<hibernate-mapping package="hibernate.entity">
        ...
    <class name="Phone" table="T_PHONE">
        <id name="id" column="ID">
            <generator class="sequence"/>
         </id>
         <property name="number" column="NUMBER"/>
         <many-to-one name="person" column="ID_PERSON" unique="false" cascade="all"/>
     </class>
     <class name="Person" table="T_PERSON">
          <id name="id" column="ID">
              <generator class="sequence"/>
          </id>
          <discriminator column="PERSON_TYPE" type="string"/>
          <property name="firstName" column="FIRST_NAME"/>
          <property name="lastName" column="LAST_NAME"/>
          <many-to-one name="idCard" column="ID_ID_CARD" unique="true" cascade="all"/>
                   <subclass name="Geek" extends="Person">
                <property name="favouriteProgrammingLanguage" column="FAV_PROG_LANG"/>
           </subclass>
       </class>
</hibernate-mapping>

В приведенном выше листинге приведено определение сопоставления для класса Phone . Помимо обычного идентификатора ( id ), который генерируется с использованием последовательности и number поля, это определение также содержит элемент many-to-one . В отличие от отношения «один к одному», которое мы видели ранее, для атрибута unique установлено значение false . Кроме того, column атрибута определяет имя столбца внешнего ключа и значение cascade атрибута, как Hibernate должен каскадировать операции с этим отношением.

После выполнения описанной выше конфигурации будут распечатаны следующие операторы DDL:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...
Hibernate:
    drop table T_PERSON if exists
Hibernate:
    drop table T_PHONE if exists
...
Hibernate:
    create table T_PERSON (
        ID bigint not null,
        PERSON_TYPE varchar(255) not null,
        FIRST_NAME varchar(255),
        LAST_NAME varchar(255),
        ID_ID_CARD bigint,
        FAV_PROG_LANG varchar(255),
        primary key (ID)
    )
Hibernate:
    create table T_PHONE (
        ID bigint not null,
        NUMBER varchar(255),
        ID_PERSON bigint,
        primary key (ID)
    )
...
Hibernate:
    alter table T_PHONE
        add constraint FK_dvxwd55q1bax99ibyw4oxa8iy
        foreign key (ID_PERSON)
        references T_PERSON
...

Рядом с таблицей T_PERSON Hibernate теперь также создает новую таблицу T_PHONE с тремя столбцами ID , NUMBER и ID_PERSON . Поскольку в последнем столбце хранится ссылка на Person , Hibernate также добавляет ограничение внешнего ключа к таблице T_PHONE которое указывает на ID столбца таблицы T_PERSON .

Чтобы добавить номер телефона одному из существующих людей, мы сначала загружаем определенного человека, а затем добавляем телефон:

01
02
03
04
05
06
07
08
09
10
session.getTransaction().begin();
List resultList = session.createQuery("from Person as person where person.firstName = ?").setString(0, "Homer").list();
for (Person person : resultList) {
    Phone phone = new Phone();
    phone.setNumber("+49 1234 456789");
    session.persist(phone);
    person.getPhones().add(phone);
    phone.setPerson(person);
}
session.getTransaction().commit();

В этом примере показано, как загрузить человека из хранилища данных с помощью языка запросов Hibernate (HQL). Подобно SQL этот запрос состоит из предложения from и where. На столбец FIRST_NAME нет ссылки с использованием его имени SQL. Вместо этого используется имя поля / свойства Java. Такие параметры, как имя, могут быть переданы в запрос с помощью setString() .

Далее код перебирает найденных лиц (должен быть только один) и создает новый экземпляр Phone который добавляется в набор телефонов найденного человека. Ссылка с телефона на человека также устанавливается до совершения транзакции. Выполнив этот код, база данных выглядит следующим образом:

1
2
3
4
5
6
7
sql> select * from t_person where first_name = 'Homer';
ID | PERSON_TYPE             | FIRST_NAME | LAST_NAME | ID_ID_CARD | FAV_PROG_LANG
1  | hibernate.entity.Person | Homer      | Simpson   | 2          | null
 
sql> select * from t_phone;
ID | NUMBER          | ID_PERSON
6  | +49 1234 456789 | 1

Наборы результатов двух приведенных выше операторов выбора показывают, что строка в T_PHONE связана с выбранной строкой в T_PERSON поскольку она содержит идентификатор человека с именем «Гомер» в столбце ID_ID_PERSON .

5.3. ManyToMany

Следующее интересное отношение — отношение «многие ко многим». В этом случае многие объекты типа A могут принадлежать многим объектам типа B и наоборот. На практике это, например, случай с фанатами и проектами. Один компьютерщик может работать в нескольких проектах (одновременно или последовательно), а один проект может состоять из нескольких компьютеров. Поэтому вводится новая сущность Project :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Project {
    private Long id;
    private String title;
    private Set geeks = new HashSet();
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getTitle() {
        return title;
    }
 
    public void setTitle(String title) {
        this.title = title;
    }
 
    public Set getGeeks() {
        return geeks;
    }
 
    public void setGeeks(Set geeks) {
        this.geeks = geeks;
    }
}

Он состоит рядом с идентификатором ( id ) заголовка и набором гиков. С другой стороны отношения у класса Geek есть множество проектов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class Geek extends Person {
    private String favouriteProgrammingLanguage;
    private Set projects = new HashSet();
 
    public String getFavouriteProgrammingLanguage() {
            return favouriteProgrammingLanguage;
    }
 
    public void setFavouriteProgrammingLanguage(String favouriteProgrammingLanguage) {
        this.favouriteProgrammingLanguage = favouriteProgrammingLanguage;
    }
 
    public Set getProjects() {
        return projects;
    }
 
    public void setProjects(Set projects) {
        this.projects = projects;
    }
}

Для поддержки такого рода отношений файл сопоставления должен быть изменен следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<hibernate-mapping package="hibernate.entity">
       ...
    <class name="Project" table="T_PROJECT">
       <id name="id" column="ID">
           <generator class="sequence"/>
        </id>
        <property name="title" column="TITLE"/>
        <set name="geeks" table="T_GEEKS_PROJECTS">
           <key column="ID_PROJECT"/>
           <many-to-many column="ID_GEEK" class="Geek"/>
        </set>
      </class>
      <class name="Person" table="T_PERSON">
         <id name="id" column="ID">
             <generator class="sequence"/>
         </id>
         <discriminator column="PERSON_TYPE" type="string"/>
         <property name="firstName" column="FIRST_NAME"/>
         <property name="lastName" column="LAST_NAME"/>
         <many-to-one name="idCard" column="ID_ID_CARD" unique="true" cascade="all"/>
         <subclass name="Geek" extends="Person">
              <property name="favouriteProgrammingLanguage" column="FAV_PROG_LANG"/>
              <set name="projects" inverse="true">
                   <key column="ID_GEEK"/>
                   <many-to-many column="ID_PROJECT" class="Project"/>
               </set>
        </subclass>
      </class>
</hibernate-mapping>

Прежде всего мы видим новый класс Project который сопоставлен с таблицей T_PROJECT . Его уникальный идентификатор хранится в поле id а title поля — в столбце TITLE . Набор элементов XML определяет одну сторону отображения: элементы внутри набора geeks должны храниться в отдельной таблице с именем T_GEEKS_PROJECTS со столбцами ID_PROJECT и ID_GEEK . С другой стороны отношения элемент XML, set внутри subclass для Geek определяет обратное отношение ( inverse="true" ). С этой стороны поле в классе Geek называется projects а ссылочный класс — Project .

Результирующие операторы для создания таблиц выглядят так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...
Hibernate:
    drop table T_GEEKS_PROJECTS if exists
Hibernate:
    drop table T_PROJECT if exists
...
Hibernate:
    create table T_GEEKS_PROJECTS (
        ID_PROJECT bigint not null,
        ID_GEEK bigint not null,
        primary key (ID_PROJECT, ID_GEEK)
    )
Hibernate:
    create table T_PROJECT (
        ID bigint not null,
        TITLE varchar(255),
        primary key (ID)
    )
...
Hibernate:
    alter table T_GEEKS_PROJECTS
        add constraint FK_2kp3f3tq46ckky02pshvjngaq
        foreign key (ID_GEEK)
        references T_PERSON
Hibernate:
    alter table T_GEEKS_PROJECTS
        add constraint FK_36tafu1nw9j5o51d21xm5rqne
        foreign key (ID_PROJECT)
        references T_PROJECT
...

Эти операторы создают новые таблицы T_PROJECT а также T_GEEKS_PROJECTS . Таблица T_PROJECT состоит из ID столбцов и TITLE посредством чего значения в ID столбца упоминаются в новой таблице T_GEEKS_PROJECTS в ее столбце ID_PROJECT . Второй внешний ключ в этой таблице указывает на первичный ключ T_PERSON .

Чтобы вставить проект с несколькими гиками, которые могут программировать на Java, в хранилище данных, можно использовать следующий код:

01
02
03
04
05
06
07
08
09
10
11
session.getTransaction().begin();
List resultList = session.createQuery("from Geek as geek
    where geek.favouriteProgrammingLanguage = ?").setString(0, "Java").list();
Project project = new Project();
project.setTitle("Java Project");
for (Geek geek : resultList) {
    project.getGeeks().add(geek);
    geek.getProjects().add(project);
}
session.save(project);
session.getTransaction().commit();

Первоначальный запрос выбирает всех гиков, для которых «Java» является их любимым языком программирования. Затем создается новый экземпляр Project и все гики, которые находятся в наборе результатов запроса, добавляются в набор гиков проекта. С другой стороны отношения проект добавляется в набор проектов для гика. Наконец проект сохраняется, и транзакция фиксируется.

После выполнения этого кода база данных выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
sql> select * from t_person;
ID | PERSON_TYPE             | FIRST_NAME | LAST_NAME | ID_ID_CARD | FAV_PROG_LANG
1  | hibernate.entity.Person | Homer      | Simpson   | 2          | null
3  | hibernate.entity.Geek   | Gavin      | Coffee    | null       | Java
4  | hibernate.entity.Geek   | Thomas     | Micro     | null       | C#
5  | hibernate.entity.Geek   | Christian  | Cup       | null       | Java
 
sql> select * from t_project;
ID | TITLE
7  | Java Project
 
sql> select * from t_geeks_projects;
ID_PROJECT | ID_GEEK
7          | 5
7          | 3

Первый выбор показывает, что только два вундеркинда с id 3 и 5 указали, что Java является их любимым языком программирования. Следовательно, проект с названием «Java Project» (id: 7) состоит из двух вундеркиндов с идентификаторами 3 и 5 (последний оператор выбора).

5.4. Составная часть

Правила объектно-ориентированного проектирования предлагают выделять часто используемые поля в отдельный класс. Например, класс Project указанный выше, по-прежнему пропускает дату начала и окончания. Но так как такой период времени можно использовать и для других объектов, мы можем создать новый класс с именем Period который инкапсулирует два поля startDate и endDate :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Period {
    private Date startDate;
    private Date endDate;
 
    public Date getStartDate() {
        return startDate;
    }
 
    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }
 
    public Date getEndDate() {
        return endDate;
    }
 
    public void setEndDate(Date endDate) {
        this.endDate = endDate;
    }
}
 
public class Project {
    ...
    private Period period;
    ...
    public Period getPeriod() {
        return period;
    }
 
    public void setPeriod(Period period) {
        this.period = period;
    }
}

Но мы не хотим, чтобы Hibernate создавал отдельную таблицу для периода, поскольку каждый Project должен иметь только одну дату начала и окончания, и мы хотим обойти дополнительное объединение. В этом случае Hibernate может сопоставить два поля во встроенном классе Period с той же таблицей, что и класс Project :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<hibernate-mapping package="hibernate.entity">
      ...
      <class name="Project" table="T_PROJECT">
          <id name="id" column="ID">
                <generator class="sequence"/>
          </id>
          <property name="title" column="TITLE"/>
          <set name="geeks" table="T_GEEKS_PROJECTS">
                 <key column="ID_PROJECT"/>
                 <many-to-many column="ID_GEEK" class="Geek"/>
           </set>
           <component name="period">
                  <property name="startDate" column="START_DATE"/>
                  <property name="endDate" column="END_DATE"/>
           </component>
         </class>
         ...
</hibernate-mapping>

Способ сопоставления этого встроенного класса с полями таблицы T_PROJECT заключается в использовании элемента component и предоставлении имени поля в классе Project для атрибута name . Два поля класса Period затем просто объявляются как свойства component .

Это приводит к следующему утверждению DDL:

01
02
03
04
05
06
07
08
09
10
...
Hibernate:
    create table T_PROJECT (
        ID bigint not null,
        TITLE varchar(255),
        START_DATE timestamp,
        END_DATE timestamp,
        primary key (ID)
    )
...

Хотя поля для START_DATE и END_DATE находятся в отдельном классе, Hibernate добавляет их в таблицу T_PROJECT . Следующий код создает новый проект и добавляет к нему точку:

1
2
3
4
5
6
7
Project project = new Project();
project.setTitle("Java Project");
Period period = new Period();
period.setStartDate(new Date());
project.setPeriod(period);
...
session.save(project);

Это приводит к следующей ситуации с данными:

1
2
3
sql> select * from t_project;
ID | TITLE        | START_DATE              | END_DATE
7  | Java Project | 2015-01-01 19:45:12.274 | null

Для загрузки периода вместе с проектом не нужно писать никакого дополнительного кода, период автоматически загружается и инициализируется:

1
2
3
4
5
List projects = session.createQuery("from Project as p where p.title = ?")
    .setString(0, "Java Project").list();
for (Project project : projects) {
    System.out.println("Project: " + project.getTitle() + " starts at " + project.getPeriod().getStartDate());
}

На случай, если в базе данных для всех полей периода установлено NULL , Hibernate также устанавливает null ссылку на Period .

6. Пользовательские типы данных

Например, при работе с устаревшей базой данных некоторые столбцы могут быть смоделированы не так, как в Hibernate. Например, тип данных Boolean отображается в базе данных H2 на тип boolean . Если исходная группа разработчиков решила отобразить логические значения, используя строку со значениями «0» и «1», Hibernate позволяет реализовать определяемые пользователем типы, которые используются для отображения.

Hibernate определяет интерфейс org.hibernate.usertype.UserType который должен быть реализован:

01
02
03
04
05
06
07
08
09
10
11
12
13
public interface UserType {
    int[] sqlTypes();
    Class returnedClass();
    boolean equals(Object var1, Object var2) throws HibernateException;
    int hashCode(Object var1) throws HibernateException;
    Object nullSafeGet(ResultSet var1, String[] var2, SessionImplementor var3, Object var4) throws HibernateException, SQLException;
    void nullSafeSet(PreparedStatement var1, Object var2, int var3, SessionImplementor var4) throws HibernateException, SQLException;
    Object deepCopy(Object var1) throws HibernateException;
    boolean isMutable();
    Serializable disassemble(Object var1) throws HibernateException;
    Object assemble(Serializable var1, Object var2) throws HibernateException;
    Object replace(Object var1, Object var2, Object var3) throws HibernateException;
}

Простые реализации тех методов, которые не являются специфичными для нашей проблемы, показаны ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Override
public boolean equals(Object x, Object y) throws HibernateException {
    if (x == null) {
        return y == null;
    } else {
        return y != null && x.equals(y);
    }
}
 
@Override
public int hashCode(Object o) throws HibernateException {
    return o.hashCode();
}
 
@Override
public Object deepCopy(Object o) throws HibernateException {
    return o;
}
 
@Override
public boolean isMutable() {
    return false;
}
 
@Override
public Serializable disassemble(Object o) throws HibernateException {
    return (Serializable) o;
}
 
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
    return cached;
}
 
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
    return original;
}

Интересной частью UserType являются методы nullSafeGet() и nullSafeSet() :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public Object nullSafeGet(ResultSet resultSet, String[] strings,
    SessionImplementor sessionImplementor, Object o) throws HibernateException, SQLException {
    String str = (String) StringType.INSTANCE.nullSafeGet(resultSet, strings[0], sessionImplementor, o);
    if ("1".equals(str)) {
        return Boolean.TRUE;
    }
    return Boolean.FALSE;
}
 
@Override
public void nullSafeSet(PreparedStatement preparedStatement, Object value,
    int i, SessionImplementor sessionImplementor) throws HibernateException, SQLException {
    String valueToStore = "0";
    if (value != null) {
        Boolean booleanValue = (Boolean) value;
        if (booleanValue.equals(Boolean.TRUE)) {
            valueToStore = "1";
        }
    }
    StringType.INSTANCE.nullSafeSet(preparedStatement,valueToStore, i, sessionImplementor);
}

Метод nullSafeGet() использует реализацию StringType Hibernate для извлечения строкового представления логического значения из ResultSet базового запроса. Если возвращаемая строка равна «1», метод возвращает «true», в противном случае он возвращает «false».

Прежде чем insertоператор может быть выполнен, логическое значение, переданное как параметр value, должно быть «декодировано» либо в строку «1», либо в строку «0». Затем метод nullSafeSet()использует StringTypeреализацию Hibernate для установки этого строкового значения в PreparedStatement.

Наконец, мы должны сказать Hibernate, из какого типа объекта возвращается nullSafeGet()и какой тип столбца следует использовать для этого типа:

1
2
3
4
5
6
7
8
9
@Override
public int[] sqlTypes() {
    return new int[]{ Types.VARCHAR };
}
 
@Override
public Class returnedClass() {
    return Boolean.class;
}

После реализации UserTypeинтерфейса экземпляр этого класса теперь может быть передан Configuration:

1
2
3
4
Configuration configuration = new Configuration();
configuration.configure("hibernate.cfg.xml");
configuration.registerTypeOverride(new MyBooleanType(), new String[]{"MyBooleanType"});
...

MyBooleanTypeЗдесь наша реализация UserTypeинтерфейса, тогда как Stringмассив определяет, как ссылаться на этот тип в файле отображения:

01
02
03
04
05
06
07
08
09
10
11
<hibernate-mapping package="hibernate.entity">
      <class name="IdCard" table="T_ID_CARD">
           <id name="id" column="ID">
               <generator class="sequence"/>
           </id>
           <property name="idNumber" column="ID_NUMBER"/>
           <property name="issueDate" column="ISSUE_DATE"/>
           <property name="valid" column="VALID" type="MyBooleanType"/>
        </class>
        ...
</hibernate-mapping>

Как видно из приведенного выше фрагмента, новый тип «MyBooleanType» используется для логического свойства таблицы T_ID_CARD:

1
2
3
sql> select * from t_id_card;
ID | ID_NUMBER | ISSUE_DATE              | VALID
2  | 4711      | 2015-03-27 11:49:57.533 | 1

7. Перехватчики

Проект может прийти с требованием, чтобы для каждой сущности / таблицы отслеживалась временная метка его создания и его последнее обновление. Установка этих двух значений для каждого объекта во всех операциях вставки и обновления является довольно утомительной задачей. Поэтому Hibernate предлагает возможность реализации перехватчиков, которые вызываются до выполнения операции вставки или обновления. Таким образом, код для установки метки времени создания и обновления может быть извлечен в одно место в базе кода, и его не нужно копировать во все места, где это будет необходимо.

В качестве примера мы собираемся реализовать контрольный журнал, который отслеживает создание и обновление Projectсущности. Это можно сделать, расширив класс EmptyInterceptor:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class AuditInterceptor extends EmptyInterceptor {
 
    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state,
        String[] propertyNames, Type[] types) {
        if (entity instanceof Auditable) {
            for ( int i=0; i < propertyNames.length; i++ ) {
                if ( "created".equals( propertyNames[i] ) ) {
                    state[i] = new Date();
                    return true;
                }
            }
            return true;
        }
        return false;
    }
 
    @Override
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState,
        Object[] previousState, String[] propertyNames, Type[] types) {
        if (entity instanceof Auditable) {
            for ( int i=0; i < propertyNames.length; i++ ) {
                if ( "lastUpdate".equals( propertyNames[i] ) ) {
                    currentState[i] = new Date();
                    return true;
                }
            }
            return true;
        }
        return false;
    }
}

Поскольку класс EmptyInterceptorуже реализует все методы, определенные в интерфейсе Interceptor, нам нужно только переопределить методы onSave()и onFlushDirty(). Для того , чтобы легко найти все объекты , которые имеют поле createdи lastUpdateвыделим методы получения и установки для этих объектов в отдельный интерфейс под названием Auditable:

1
2
3
4
5
6
public interface Auditable {
    Date getCreated();
    void setCreated(Date created);
    Date getLastUpdate();
    void setLastUpdate(Date lastUpdate);
}

С этим интерфейсом легко проверить, имеет ли тип, переданный в перехватчик, тип Auditable. К сожалению, мы не можем изменить сущность напрямую через методы getter и setter, но мы должны использовать два массива propertyNamesи state. В массиве propertyNamesмы должны найти свойство created( lastUpdate) и использовать его индекс для установки соответствующего элемента в массиве state( currentState).

Без соответствующих определений свойств в файле сопоставления Hibernate не будет создавать столбцы в таблицах. Следовательно, файл отображения должен быть обновлен:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
<hibernate-mapping>
        ...
    <class name="Project" table="T_PROJECT">
        <id name="id" column="ID">
            <generator class="sequence"/>
        </id>
        <property name="title" column="TITLE"/>
        <set name="geeks" table="T_GEEKS_PROJECTS">
             <key column="ID_PROJECT"/>
             <many-to-many column="ID_GEEK" class="Geek"/>
         </set>
         <component name="period">
              <property name="startDate" column="START_DATE"/>
              <property name="endDate" column="END_DATE"/>
         </component>
         <property name="created" column="CREATED" type="timestamp"/>
         <property name="lastUpdate" column="LAST_UPDATE" type="timestamp"/>
     </class>
          ...
</hibernate-mapping>

Как можно видеть из фрагмента кода выше, двух новых свойств createdи lastUpdateимеют типа timestamp:

1
2
3
4
5
6
sql> select * from t_person;
ID | PERSON_TYPE             | FIRST_NAME | LAST_NAME | CREATED                 | LAST_UPDATE | ID_ID_CARD | FAV_PROG_LANG
1  | hibernate.entity.Person | Homer      | Simpson   | 2015-01-01 19:45:42.493 | null        | 2          | null
3  | hibernate.entity.Geek   | Gavin      | Coffee    | 2015-01-01 19:45:42.506 | null        | null       | Java
4  | hibernate.entity.Geek   | Thomas     | Micro     | 2015-01-01 19:45:42.507 | null        | null       | C#
5  | hibernate.entity.Geek   | Christian  | Cup       | 2015-01-01 19:45:42.507 | null        | null       | Java

8. Загрузите исходный код Hibernate.

Это был учебник Hibernate.