ПРИМЕЧАНИЕ РЕДАКЦИИ: В этом посте мы представляем всеобъемлющее руководство по 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.