Глава 1. Руководство
Оглавление-
1.1. Часть 1 - Первое приложение Hibernate
- 1.1.1. Установка
- 1.1.2. Первый класс
- 1.1.3. Файл отображений
- 1.1.4. Конфигурирование Hibernate
- 1.1.5. Сборка с Maven
- 1.1.6. Запуск и помошники
- 1.1.7. Загрузка и хранение объектов
-
1.2. Часть 2 - Отображение ассоциаций
- 1.2.1. Отображение для класса Person
- 1.2.2. Однонаправленная ассоциация на основе Set
- 1.2.3. Работа с ассоциациями
- 1.2.4. Коллекия значений
- 1.2.5. Двунаправленные ассоциации
- 1.2.6. Работа двунаправленных связей
-
1.3. Часть 3 - приложение Менеджер событий
- 1.3.1. Написание основного сервлета
- 1.3.2. Обработка и визуализация
- 1.3.3. Развёртывание и тестирование
- 1.4. Резюме
Предназначенная для новых пользователей, эта глава содержит пошаговое введение в Hibernate,
начиная с простого приложения, используя базу данных в памяти (in-memory).
Учебник основан на более раннем учебнике, разработанном Майклом Глоэлем.
Весь код содержится в tutorials/web
источника проекта.
Важно
В этом учебнике предполагается, что пользователь знает как Java, так и SQL. Если у вас есть ограниченное знание JAVA или SQL, рекомендуется начать с хорошего ознакомления с этой технологией, прежде чем пытаться изучить Hibernate.
Заметка
Дистрибутив содержит другое приложение-пример в директории tutorial/eg
1.1. Часть 1 - Первое приложение Hibernate
В этом примере мы создадим небольшое приложение базы данных, которое может хранить события, которые мы хотим посетить, и информацию о хостах(hosts) этих событий.
Заметка
Несмотря на то, что вы можете использовать любую базу данных, с которой вам удобно пользоваться, мы будем использовать HSQLDB (базу данных в памяти, базу данных Java), чтобы избежать описания установки/настройки каких-либо конкретных серверов баз данных.
1.1.1. Установка
Первое, что нам нужно сделать, это настроить среду разработки. Мы будем использовать
«стандартную компоновку», которую пропагандируют многие инструменты сборки,
такие как Maven. У Maven, в частности,
есть хороший ресурс, описывающий этот
макет.
Поскольку этот учебник должен быть веб-приложением, мы будем создавать и использовать каталоги
src/main/java, src/main/resources
и src/main webapp
.
Мы будем использовать Maven в этом учебном пособии, используя возможности его транзитивных функций управления зависимостями, а также способность многих IDE автоматически настраивать проект для нас на основе дескриптора maven.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.hibernate.tutorials</groupId> <artifactId>hibernate-tutorial</artifactId> <version>1.0.0-SNAPSHOT</version> <name>First Hibernate Tutorial</name> <build> <!-- we dont want the version to be part of the generated war file name --> <finalName>${artifactId}</finalName> </build> <dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> </dependency> <!-- Because this is a web app, we also have a dependency on the servlet api. --> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> </dependency> <!-- Hibernate uses slf4j for logging, for our purposes here use the simple backend --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> </dependency> <dependency> <groupId>javassist</groupId> <artifactId>javassist</artifactId> </dependency> </dependencies> </project>
Совет
Это не требование использовать Maven. Если вы хотите использовать что-то еще
для сборки проекта (например, Ant), макет останется прежним.
Единственное изменение заключается в том, что вам нужно будет вручную добавить
все необходимые зависимости. Если вы используете что-то вроде Ivy,
обеспечивающее транзитивное управление зависимостями, вы все равно будете добавлять зависимости,
упомянутые ниже. В противном случае вам нужно будет указать все зависимости,
как явные, так и транзитивные, добавив их в путь к классу проекта.
Если вы работаете из дистрибутива Hibernate, это будет означать hibernate3.jar,
все артефакты в каталоге lib/required
и все файлы из каталога
lib/bytecode/cglib
или lib/bytecode/javassist
кроме того,
вам понадобятся как servlet-api jar, так и один из бэкэндов регистрации slf4j.
Сохраните этот файл как pom.xml
в корневом каталоге проекта.
1.1.2. Первый класс
package org.hibernate.tutorial.domain;
import java.util.Date;
public class Event { private Long id; private String title; private Date date;
public Event() { }
public Long getId() { return id; }
private void setId(Long id) { this.id = id; }
public Date getDate() { return date; }
public void setDate(Date date) { this.date = date; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; } }
Этот класс использует стандартные соглашения JavaBean об именах методов getter и setter для полей, а также конфиденциальную видимость полей. Хотя это рекомендуемый дизайн, он не требуется. Hibernate также может напрямую обращаться к полям, преимущество методов доступа — это надежность для рефакторинга.
Свойство id содержит уникальное значение идентификатора для определенного события. Все постоянные классы сущностей (также как и менее важные зависимые классы) нуждаются в таком идентификаторе, если мы хотим использовать полный набор функций Hibernate. Фактически, большинство приложений, особенно веб-приложений, должны различать объекты по идентификатору, поэтому вы должны рассматривать это как особенность, а не как ограничение. Однако мы обычно не манипулируем идентификацией объекта, поэтому метод setter должен быть закрытым. Только Hibernate присваивает идентификаторы при сохранении объекта. Hibernate может напрямую обращаться к общедоступным, приватным и защищенным методам доступа, а также к открытым, закрытым и защищенным полям. Выбор зависит от вас, и вы можете соответствовать ему в соответствии с дизайном вашего приложения.
Конструктор без аргументов является обязательным требованием для всех постоянных (persistent) классов; Hibernate должен создавать объекты для вас, используя Java Reflection. Конструктор может быть закрытым, однако для генерации прокси-сервера во время выполнения и эффективного извлечения данных без инструментария байт-кода требуется пакетная (package) или общедоступная (public) видимость.
Смотри этот файл в директории src/main/java/org/hibernate/tutorial/domain
1.1.3. Файл отображений (mapping)
Hibernate должен знать, как загружать и хранить объекты постоянного (persistent) класса. Здесь вступает в игру файл отображения (mapping) Hibernate. Файл отображения сообщает Hibernate, какую таблицу в базе данных он должен получить, и какие столбцы в этой таблице он должен использовать.
Основная структура файла отображения выглядит так:
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="org.hibernate.tutorial.domain"> [...] </hibernate-mapping>
Hibernate DTD сложный. Вы можете использовать его для автоматического завершения элементов и атрибутов XML-отображения в вашем редакторе или в среде IDE. Открытие DTD-файла в текстовом редакторе — это самый простой способ получить обзор всех элементов и атрибутов, а также просмотреть значения по умолчанию, а также некоторые комментарии. Hibernate не будет загружать DTD-файл из Интернета, но сначала просмотрите его из пути к классам приложения. Файл DTD включён в hibernate-core.jar (он также включён в hibernate3.jar, если используется дистрибутивный пакет).
Важно
Мы опустим декларацию DTD в будущих примерах, чтобы сократить код.
Между двумя тегами hibernate-mapping
включают элемент class
.
Все постоянные (persistent) классы сущностей (опять же, впоследствии могут быть зависимые классы,
которые не являются первоклассными сущностями) необходимо отображение с таблицей
в базе данных SQL:
<hibernate-mapping package="org.hibernate.tutorial.domain"> <class name="Event" table="EVENTS">
</class> </hibernate-mapping>
До сих пор мы говорили Hibernate, как сохранять и загружать объект класса
Event
в таблицу EVENTS
. Каждый экземпляр теперь представлен строкой
в этой таблице. Теперь мы можем продолжить, отображая свойство уникального идентификатора
на первичный ключ таблиц. Поскольку мы не хотим заботиться об обработке этого
идентификатора, мы настраиваем стратегию генерации идентификатора Hibernate для суррогатного
столбца первичного ключа:
<hibernate-mapping package="org.hibernate.tutorial.domain"> <class name="Event" table="EVENTS"> <id name="id" column="EVENT_ID"> <generator class="native" /> </id> </class> </hibernate-mapping>
Элемент id
является объявлением свойства идентификатора. Атрибут отображения
name="id"
объявляет имя свойства JavaBean и сообщает Hibernate использовать методы
getId()
и setId()
для доступа к свойству. Атрибут column
указывает Hibernate, в каком столбце таблицы EVENTS
хранится значение первичного ключа.
Вложенный элемент generator
определяет стратегию генерации идентификатора (также как генерируются
значения идентификатора?). В этом случае мы выбираем native
,
который предлагает уровень переносимости в зависимости от настроенного диалекта базы данных.
Hibernate поддерживает идентификаторы, создаваемые базой данных, глобально уникальные,
а также назначенные приложением. Генерация значения идентификатора также является одной
из многих точек расширения Hibernate, и вы можете подключиться к своей собственной стратегии.
Совет
native
больше не рассматривается как лучшая стратегия с точки зрения переносимости.
Для дальнейшего обсуждения, см.
Глава 27.4 «Генерация идентификатора»
Наконец, нам нужно сообщить Hibernate об остальных свойствах класса объектов. По умолчанию свойства класса не считаются постоянными (persistent):
<hibernate-mapping package="org.hibernate.tutorial.domain"> <class name="Event" table="EVENTS"> <id name="id" column="EVENT_ID"> <generator class="native" /> </id> <property name="date" type="timestamp" column="EVENT_DATE" /> <property name="title" /> </class> </hibernate-mapping>
Подобно элементу id
, атрибут name
элемента свойства сообщает Hibernate,
какие методы getter и setter использовать. В этом случае Hibernate будет искать методы
getDate(), setDate(), getTitle()
и setTitle()
.
Заметка
Почему отображение свойства date
включает атрибут column
,
но title
не указан? Без атрибута column
Hibernate
по умолчанию использует имя свойства как имя столбца. Это работает для title
,
однако date
является зарезервированным ключевым словом в большинстве баз данных,
поэтому вам нужно будет отобразать ее с другим именем.
В title
также отсутствует атрибут type
. Типы, объявленные
и используемые в файлах отображений, не являются типами данных Java;
они также не являются типами баз данных SQL. Эти типы называются
типами отображения Hibernate (Hibernate mapping types), которые могут переводить
типы данных Java в SQL и наоборот. Опять же, Hibernate попытается определить
правильный тип преобразования и отображения, если атрибут type
отсутствует в отображении.
В некоторых случаях это автоматическое обнаружение с использованием Reflection на Java-классе
может не иметь значения по умолчанию, которое вы ожидаете.
Это относится к свойству date
. Hibernate не может знать, должно ли свойство
java.util.Date
отображаться на SQL date, timestamp
или time
.
Полная информация о дате и времени сохраняется путем отображения свойства с преобразователем
временной метки.
Совет
Hibernate определяет типа отображения, используя отражение при обработке файлов отображения. Это может занять много времени и ресурсов, поэтому, если производительность важна, вам следует рассмотреть явное определение используемого типа.
Сохраните файл отображения как src/main/resources/org/hibernate/tutorial/domain/Event.hbm.xml.
1.1.4. Конфигурирование Hibernate
На этом этапе у вас должен быть постоянный (persistent) класс и его файл отображения. Настало время настроить Hibernate. Сначала давайте настроим HSQLDB для запуска в «режиме сервера».
Заметка
Мы делаем это так, чтобы данные оставались между запусками.
Мы будем использовать плагин Maven exec для запуска сервера HSQLDB, выполнив: mvn exec: java -Dexec.mainClass="org.hsqldb.Server" -Dexec.args="- database.0 file: target/data/tutorial" Вы увидите, что он запускается и привязывается к сокету TCP/IP. Здесь наше приложение будет подключаться позже. Если вы хотите начать с новой базой данных, выключите HSQLDB, удалите все файлы в каталоге target/data и снова запустите HSQLDB.
Hibernate будет подключаться к базе данных от имени вашего приложения, поэтому он должен знать,
как получить соединения. Для этого урока мы будем использовать автономный пул соединений
(в отличие от javax.sql.DataSource
). Hibernate поставляется с поддержкой двух
сторонних пулов соединений JDBC с открытым исходным кодом:
c3p0
и proxool.
Однако для этого учебника мы будем использовать встроенный пул соединений Hibernate.
Осторожно
Встроенный пул соединений Hibernate никоим образом не предназначен для использования в продакшене. Он не имеет нескольких функций, имеющихся в любом приличном пуле подключений.
Для конфигурации Hibernate мы можем использовать простой файл hibernate.properties
,
более сложный файл hibernate.cfg.xml
или даже полную программную настройку.
Большинство пользователей предпочитают файл конфигурации XML:
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <!-- Database connection settings --> <property name="connection.driver_class">org.hsqldb.jdbcDriver</property> <property name="connection.url">jdbc:hsqldb:hsql://localhost</property> <property name="connection.username">sa</property> <property name="connection.password"></property>
<!-- JDBC connection pool (use the built-in) --> <property name="connection.pool_size">1</property>
<!-- SQL dialect --> <property name="dialect">org.hibernate.dialect.HSQLDialect</property>
<!-- Enable Hibernate's automatic session context management --> <property name="current_session_context_class">thread</property>
<!-- Disable the second-level cache --> <property name="cache.provider_class">org.hibernate.cache.internal.NoCacheProvider</property>
<!-- Echo all executed SQL to stdout --> <property name="show_sql">true</property>
<!-- Drop and re-create the database schema on startup --> <property name="hbm2ddl.auto">update</property>
<mapping resource="org/hibernate/tutorial/domain/Event.hbm.xml"/> </session-factory> </hibernate-configuration>
Заметка
Обратите внимание, что этот файл конфигурации указывает другой DTD
Вы настраиваете SessionFactory
для Hibernate. SessionFactory является глобальной фабрикой,
ответственной за конкретную базу данных. Если у вас несколько баз данных, для упрощения запуска
вы должны использовать несколько конфигураций <session-factory>
в нескольких файлах конфигурации.
Первые четыре элемента свойств содержат необходимую конфигурацию для соединения JDBC. Элемент свойства dialect указывает конкретный вариант SQL, генерируемый Hibernate.
Совет
В большинстве случаев Hibernate способен правильно определить, какой диалект использовать. См. Раздел 27.3 «Разрешение диалекта» для получения дополнительной информации.
В этом контексте особенно полезно автоматическое управление сессией Hibernate для контекстов
персистентности. Опция hbm2ddl.auto
включает автоматическую генерацию схем баз данных
непосредственно в базу данных. Это также можно отключить, удалив параметр конфигурации или перенаправив
его в файл с помощью задачи SchemaExport
в Ant. Наконец, добавьте файлы отображений
для постоянных (persistent) классов в конфигурацию.
Сохрание файл hibernate.cfg.xml
в директорию src/main/resources
.
1.1.5. Сборка с Maven
Теперь мы построим учебный пример с Maven. Вам нужно будет установить Maven;
он доступен на странице загрузки Maven.
Maven будет читать файл /pom.xml
, который мы создали ранее, и узнает,
как выполнять некоторые основные задачи проекта. Во-первых, давайте запустим цель компиляции, чтобы убедиться,
что мы все можем скомпилировать:
[hibernateTutorial]$ mvn compile [INFO] Scanning for projects... [INFO] ------------------------------------------------------------------------ [INFO] Building First Hibernate Tutorial [INFO] task-segment: [compile] [INFO] ------------------------------------------------------------------------ [INFO] [resources:resources] [INFO] Using default encoding to copy filtered resources. [INFO] [compiler:compile] [INFO] Compiling 1 source file to /home/steve/projects/sandbox/hibernateTutorial/target/classes [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESSFUL [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2 seconds [INFO] Finished at: Tue Jun 09 12:25:25 CDT 2009 [INFO] Final Memory: 5M/547M [INFO] ------------------------------------------------------------------------
1.1.6. Запуск и помошники
Настало время загрузить и сохранить некоторые объекты Event
, но сначала
вы должны завершить настройку с помощью некоторого кода инфраструктуры. Вам нужно запустить Hibernate,
создав глобальный объект org.hibernate.SessionFactory
и сохранив его где-нибудь
для легкого доступа в коде приложения. Для получения экземпляров org.hibernate.Session
используется org.hibernate.SessionFactory
. org.hibernate.Session
представляет
собой однопоточную единицу работы. org.hibernate.SessionFactory
— это потокобезопасный
глобальный объект, созданный один раз.
Мы создадим вспомогательный класс HibernateUtil
, который позаботится о запуске
и сделает доступ к org.hibernate.SessionFactory
более удобным.
package org.hibernate.tutorial.util;
import org.hibernate.SessionFactory; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.cfg.Configuration;
public class HibernateUtil {
private static final SessionFactory sessionFactory = buildSessionFactory();
private static SessionFactory buildSessionFactory() { try { // Создать SessionFactory из hibernate.cfg.xml return new Configuration().configure().buildSessionFactory( new StandardServiceRegistryBuilder().build() ); } catch (Throwable ex) { // Убедиться что исключения логируются, поскольку они могут быть потеряны System.err.println("Initial SessionFactory creation failed." + ex); throw new ExceptionInInitializerError(ex); } }
public static SessionFactory getSessionFactory() { return sessionFactory; } }
Сохрание код в src/main/java/org/hibernate/tutorial/util/HibernateUtil.java
Этот класс не только создает глобальную ссылку org.hibernate.SessionFactory
в своем статическом инициализаторе; он также скрывает тот факт, что он использует
статический синглтон. Мы могли бы также найти ссылку org.hibernate.SessionFactory
из JNDI на сервере приложений или в любом другом месте, если на то пошло.
Если вы укажете имя org.hibernate.SessionFactory
в вашей конфигурации,
Hibernate попытается связать его с JNDI под этим именем после его создания. Другой, лучший
вариант — использовать развертывание JMX и позволить контейнеру, совместимому с JMX,
создавать экземпляр и связывать HibernateService
с JNDI.
Такие расширенные варианты обсуждаются далее.
Теперь вам нужно настроить систему ведения журнала. Hibernate предоставляет логирование в двух вариантах:
log4j и JDK 1.4 logging. Большинство разработчиков предпочитают Log4j: скопируйте
log4j.properties
из каталога etc/ дистрибутива Hibernate в каталог src,
рядом с hibernate.cfg.xml
. Если вы предпочитаете иметь более подробный вывод,
чем тот, который приведен в примере конфигурации, вы можете изменить настройки.
По умолчанию в стандартный поток вывода (stdout) отображается только сообщение о запуске Hibernate.
Инфраструктура учебного проекта создана, и теперь вы готовы к реальной работе с Hibernate.
1.1.7. Загрузка и хранение объектов
Теперь мы готовы начать настоящую работу с Hibernate. Давайте начнем написание класса
EventManager
с метода main():
package org.hibernate.tutorial;
import org.hibernate.Session; import java.util.*; import org.hibernate.tutorial.domain.Event; import org.hibernate.tutorial.util.HibernateUtil;
public class EventManager {
public static void main(String[] args) { EventManager mgr = new EventManager(); if (args[0].equals("store")) { mgr.createAndStoreEvent("My Event", new Date()); } HibernateUtil.getSessionFactory().close(); }
private void createAndStoreEvent(String title, Date theDate) { Session session = HibernateUtil.getSessionFactory().getCurrentSession(); session.beginTransaction(); Event theEvent = new Event(); theEvent.setTitle(title); theEvent.setDate(theDate); session.save(theEvent); session.getTransaction().commit(); } }
В createAndStoreEvent() мы создали новый объект Event
и передали его в Hibernate.
В этот момент Hibernate позаботится о SQL и выполнит INSERT в базе данных.
Организация org.hibernate.Session
предназначена для представления единой единицы работы
(единый неделимый кусок, который необходимо выполнить). На данный момент мы будем держать
вещи простыми и предполагать индивидуальную детализацию между Hibernate org.hibernate.Session
и транзакцией базы данных. Чтобы защитить наш код от реальной базовой системы транзакций,
мы используем API-интерфейс Hibernate org.hibernate.Transaction
. В этом конкретном
случае мы используем транзакционную семантику на основе JDBC, но она также может работать с JTA.
Что делает sessionFactory.getCurrentSession()
? Во-первых, вы можете вызывать его много раз
и в любом месте, которое вам нравится, когда у вас будет
экземпляр org.hibernate.SessionFactory
. Метод getCurrentSession()
всегда
возвращает «текущую» единицу работы. Помните, что мы переключили параметр конфигурации
для этого механизма на «thread» в нашем src/main/resources/hibernate.cfg.xml
?
Из-за этого параметра контекст текущей единицы работы привязан к текущему потоку Java,
который выполняет приложение.
Важно
Hibernate предлагает три метода отслеживания текущей сессии. Метод, основанный на потоке («thread»), не предназначен для использования в продакшене; это просто полезно для прототипов и учебных приложений, такого как это. Текущее («current») отслеживание сессии обсуждается более подробно далее.
org.hibernate.Session
начинается, когда первый вызов getCurrentSession()
выполняется для текущего потока. Он привязывает Hibernate к текущему потоку. Когда транзакция
заканчивается, либо путем фиксации (commit), либо отката (rollback), Hibernate автоматически отвязывает
org.hibernate.Session
от потока и закрывает его за вас. Если вы снова вызове
getCurrentSession()
, вы получите новый org.hibernate.Session
и сможете
начать новую единицу работы.
В зависимости от объема единицы работы, должен ли Hibernate org.hibernate.Session
использоваться для выполнения одной или нескольких операций с базой данных? В приведенном
выше примере используется один org.hibernate.Session
для одной операции. Однако это чистое
совпадение; пример просто недостаточно сложный, чтобы показать какой-либо другой подход. Объем
Hibernate org.hibernate.Session
является гибким, но вы никогда не должны
проектировать ваше приложение для использования нового Hibernate org.hibernate.Session
для каждой операции с базой данных. Несмотря на то, что он используется в следующих
примерах, рассматривайте сессионную операцию (session-per-operation) как анти-шаблон.
Настоящее веб-приложение, показаное далее в этом учебнике, поможет проиллюстрировать это.
Дополнительную информацию об обработке транзакций см. в главе 13 «Транзакции и параллельное исполнение». Предыдущий пример также пропустил обработку ошибок и откат (rollback).
Чтобы запустить пример, мы будем использовать плагин Maven exec для вызова нашего класса с необходимой установкой пути к классам: mvn exec: java -Dexec.mainClass="org.hibernate.tutorial.EventManager" -Dexec.args="store"
Заметка
Возможно, вам придется сначала выполнить mvn compile.
Вы должны увидеть запуск Hibernate и, в зависимости от вашей конфигурации, много выходных данных журнала. К концу будет отображаться следующая строка:
[java] Hibernate: insert into EVENTS (EVENT_DATE, title, EVENT_ID) values (?, ?, ?)
Это INSERT, выполненный Hibernate.
Чтобы перечислить сохраненные события, к основному методу добавляется опция:
if (args[0].equals("store")) { mgr.createAndStoreEvent("My Event", new Date()); } else if (args[0].equals("list")) { List events = mgr.listEvents(); for (int i = 0; i < events.size(); i++) { Event theEvent = (Event) events.get(i); System.out.println( "Event: " + theEvent.getTitle() + " Time: " + theEvent.getDate() ); } }
Также добавлен новый метод listEvents()
:
private List listEvents() { Session session = HibernateUtil.getSessionFactory().getCurrentSession(); session.beginTransaction(); List result = session.createQuery("from Event").list(); session.getTransaction().commit(); return result; }
Здесь мы используем запрос Hibernate Query Language (HQL) для загрузки всех существующих объектов
Event
из базы данных. Hibernate сгенерирует соответствующий SQL, отправит его
в базу данных и заполнит объекты Event
данными. Вы можете создавать более сложные
запросы с помощью HQL. Дополнительную информацию см.
в главе 16, HQL: язык запросов Hibernate.
Теперь мы можем назвать нашу новую функциональность, снова используя плагин Maven exec: mvn exec: java -Dexec.mainClass="org.hibernate.tutorial.EventManager" -Dexec.args="list"
1.2. Часть 2 - Отображение ассоциаций
До сих пор мы отображали один постоянный (persistent) класс сущностей с изолированной таблицей. Давайте немного расширим пример и добавим ещё несколько классов и ассоциации. Мы добавим людей в приложение и сохраним список событий, в которых они участвуют.
1.2.1. Отображение для класса Person
Первый набросок класса Person
выглядит следующим образом:
package org.hibernate.tutorial.domain;
public class Person {
private Long id; private int age; private String firstname; private String lastname;
public Person() { ... }
// Accessor methods for all properties, private setter for 'id' }
Сохраните код в файл с именем src/main/java/org/hibernate/tutorial/domain/Person.java
Далее, создайте новый файл отображения src/main/resources/org/hibernate/tutorial/domain/Person.hbm.xml
<hibernate-mapping package="org.hibernate.tutorial.domain">
<class name="Person" table="PERSON"> <id name="id" column="PERSON_ID"> <generator class="native"/> </id> <property name="age"/> <property name="firstname"/> <property name="lastname"/> </class>
</hibernate-mapping>
Добавьте новый файл отображения в конфигурацию Hibernate:
<mapping resource="org/hibernate/tutorial/domain/Event.hbm.xml"/> <mapping resource="org/hibernate/tutorial/domain/Person.hbm.xml"/>
Создайте связь между этими двумя объектами. Люди могут участвовать в мероприятиях, а у событий — есть участники. Вопросы дизайна, с которыми вам приходится иметь дело: — это направленность, множественность и поведение коллекции.
1.2.2. Однонаправленная ассоциация на основе Set
Добавляя коллекцию событий в класс Person, вы можете легко перейти к событиям для определенного
человека, не выполняя явный запрос — вызывая Person#getEvents
.
Многозначные ассоциации представлены в Hibernate одним из контрактов Java Collection Framework;
здесь мы выбираем java.util.Set
, потому что коллекция не будет содержать повторяющиеся
элементы, а порядок не имеет отношения к нашим примерам:
public class Person {
private Set events = new HashSet();
public Set getEvents() { return events; }
public void setEvents(Set events) { this.events = events; } }
Перед отображением этой ассоциации рассмотрим другой случай. Мы могли бы просто сохранить
её как однонаправленную или создать еще одну коллекцию в Event
, если бы мы хотели,
чтобы была связь в обоих направлениях. Это необязательно с функциональной точки зрения.
Вы всегда можете выполнить явный запрос для извлечения участников для определенного события.
Этот выбор в дизайне оставлен вам, но то, что ясно из этого обсуждения, —
это множественность ассоциации: «many», указанное в обеих ассоциациях, называется ассоциацией
«многие-ко-многим» (many-to-many). Следовательно, мы используем отображение Hibernate
many-to-many:
<class name="Person" table="PERSON"> <id name="id" column="PERSON_ID"> <generator class="native"/> </id> <property name="age"/> <property name="firstname"/> <property name="lastname"/>
<set name="events" table="PERSON_EVENT"> <key column="PERSON_ID"/> <many-to-many column="EVENT_ID" class="Event"/> </set> </class>
Hibernate поддерживает широкий спектр коллекционных отображений, наиболее часто встречается Set
.
Для ассоциации «многие-ко-многим» или отношения сущности n:m требуется таблица ассоциации.
Каждая строка в этой таблице представляет собой связь между человеком и событием.
Имя таблицы объявляется с использованием атрибута table
заданного элемента.
Имя столбца идентификатора в ассоциации для стороны пользователя определяется ключевым элементом,
именем столбца для стороны события с атрибутом column
для многих-ко-многим
.
Вы также должны указать Hibernate класс объектов в вашей коллекции (класс на другой стороне
коллекции ссылок).
Таким образом, схема базы данных для этого отображения:
_____________ __________________ | | | | _____________ | EVENTS | | PERSON_EVENT | | | |_____________| |__________________| | PERSON | | | | | |_____________| | *EVENT_ID | <--> | *EVENT_ID | | | | EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID | | TITLE | |__________________| | AGE | |_____________| | FIRSTNAME | | LASTNAME | |_____________|
1.2.3. Работа с ассоциациями
Теперь мы добавим несколько людей и событий в новый метод в EventManager
:
private void addPersonToEvent(Long personId, Long eventId) { Session session = HibernateUtil.getSessionFactory().getCurrentSession(); session.beginTransaction();
Person aPerson = (Person) session.load(Person.class, personId); Event anEvent = (Event) session.load(Event.class, eventId); aPerson.getEvents().add(anEvent);
session.getTransaction().commit(); }
После загрузки Person
и Event
просто измените коллекцию, используя обычные методы
коллекций. Явный вызов update()
или save()
не нужен. Hibernate автоматически определяет,
что коллекция была изменена и нуждается в обновлении. Это называется
автоматической грязной проверкой (automatic dirty checking). Вы также можете попробовать это,
изменив имя или свойство даты любого из ваших объектов. Пока они находятся
в постоянном (persistent) состоянии, то есть связаны с определенным
org.hibernate.Session
, Hibernate контролирует любые изменения и выполняет SQL
в стиле запись-после (write-behind). Процесс синхронизации состояния памяти с базой данных происходящий,
как правило, только в конце единицы работы, называется промывкой (flushing).
В нашем коде единица работы заканчивается фиксацией (commit) или откатом (rollback) транзакции базы данных.
Вы можете загрузить человека и событие в разных единицах работы. Или вы можете изменить
объект за пределами org.hibernate.Session
, если он не находится
в постоянном (persistent) состоянии (если он был постоянным (persistent) раньше, это состояние
называется отсоединенным (detached)). Вы можете даже модифицировать коллекцию при её отсоединении:
private void addPersonToEvent(Long personId, Long eventId) { Session session = HibernateUtil.getSessionFactory().getCurrentSession(); session.beginTransaction();
Person aPerson = (Person) session .createQuery("select p from Person p left join fetch p.events where p.id = :pid") .setParameter("pid", personId) .uniqueResult(); // Eager fetch the collection so we can use it detached Event anEvent = (Event) session.load(Event.class, eventId);
session.getTransaction().commit();
// End of first unit of work aPerson.getEvents().add(anEvent); // aPerson (and its collection) is detached
// Begin second unit of work Session session2 = HibernateUtil.getSessionFactory().getCurrentSession(); session2.beginTransaction(); session2.update(aPerson); // Reattachment of aPerson
session2.getTransaction().commit(); }
Вызов update
снова сделает отсоединёный (detached) объект постоянным (persistent),
привязывая его к новой единице работы, поэтому любые изменения, внесенные вами в отсоединёном (detached)
состоянии, могут быть сохранены в базе данных. Сюда входят любые изменения (добавление/удаление),
которые вы сделали в коллекции этого объекта.
Это не очень полезно в нашем примере, но это важная концепция, которую вы можете включить
в свое приложение. Завершите это упражнение, добавив новое действие к основному методу
EventManager
и вызовите его из командной строки. Если вам нужны идентификаторы человека
и события — метод save()
возвратит их (возможно, вам придется изменить некоторые
из предыдущих методов, чтобы они возвращали этот идентификатор):
else if (args[0].equals("addpersontoevent")) { Long eventId = mgr.createAndStoreEvent("My Event", new Date()); Long personId = mgr.createAndStorePerson("Foo", "Bar"); mgr.addPersonToEvent(personId, eventId); System.out.println("Added person " + personId + " to event " + eventId); }
Это пример ассоциации между двумя одинаково важными классами: двумя сущностями. Как упоминалось ранее,
в типичной модели есть другие классы и типы, обычно «менее важные». Некоторые из них
вы уже видели, например, int
или java.lang.String
. Мы называем эти классы
типами значений (value types), а их экземпляры зависят от конкретного объекта.
Экземпляры этих типов не имеют собственной идентичности и не разделяются между сущностями.
Два человека не ссылаются на один и тот же объект firstname
,
даже если они имеют одинаковое имя. Типы значений не могут быть найдены только в JDK,
но вы также можете сами писать зависимые классы, такие как класс Address
или MonetaryAmount
. Фактически, в приложении Hibernate все классы JDK считаются типами значений.
Вы также можете создать коллекцию типов значений. Это концептуально отличается от набора ссылок на другие объекты, но на Java практически одинаково.
1.2.4. Коллекия значений
Давайте добавим коллекцию адресов электронной почты к объекту Person
. Это будет представлено
как java.util.Set
экземпляров java.lang.String
:
private Set emailAddresses = new HashSet();
public Set getEmailAddresses() { return emailAddresses; }
public void setEmailAddresses(Set emailAddresses) { this.emailAddresses = emailAddresses; }
Отображение этого Set
выглядит следующим образом:
<set name="emailAddresses" table="PERSON_EMAIL_ADDR"> <key column="PERSON_ID"/> <element type="string" column="EMAIL_ADDR"/> </set>
Разница по сравнению с более ранним отображением заключается в использовании части элемента,
которая сообщает Hibernate, что коллекция не содержит ссылок на другой объект,
а скорее представляет собой набор, элементы которого являются типами значений,
здесь конкретно типа string
. В нижнем регистре указано, что это отображение type/converter.
Снова атрибут table
элемента set
определяет имя таблицы для коллекции.
Ключевой элемент определяет имя столбца внешнего ключа в таблице коллекции.
Атрибут column
в элементе element
определяет имя столбца, в котором
значения адреса электронной почты будут фактически сохранены.
Вот обновленная схема:
_____________ __________________ | | | | _____________ | EVENTS | | PERSON_EVENT | | | ___________________ |_____________| |__________________| | PERSON | | | | | | | |_____________| | PERSON_EMAIL_ADDR | | *EVENT_ID | <--> | *EVENT_ID | | | |___________________| | EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID | <--> | *PERSON_ID | | TITLE | |__________________| | AGE | | *EMAIL_ADDR | |_____________| | FIRSTNAME | |___________________| | LASTNAME | |_____________|
Вы можете видеть, что первичный ключ таблицы коллекции на самом деле является составным ключом, который использует оба столбца. Это также подразумевает, что не должно быть одинаковых адресов электронной почты на человека, что является именно семантикой, необходимой для установки в Java.
Теперь вы можете попробовать добавить элементы в эту коллекцию, как и раньше, связывая людей и события. Это тот же код в Java:
private void addEmailToPerson(Long personId, String emailAddress) { Session session = HibernateUtil.getSessionFactory().getCurrentSession(); session.beginTransaction(); Person aPerson = (Person) session.load(Person.class, personId); // adding to the emailAddress collection might trigger a lazy load of the collection aPerson.getEmailAddresses().add(emailAddress); session.getTransaction().commit(); }
На этот раз мы не использовали запрос на выборку (fetch) для инициализации коллекции. Проверьте журнал SQL и попытайтесь оптимизировать его с помощью надежной выборки.
1.2.5. Двунаправленные ассоциации
Затем вы отобразите двунаправленную ассоциацию. Вы создадите связь между человеком и событием с обеих сторон в Java. Схема базы данных не изменяется, поэтому вы все равно будете иметь связь «многие-ко-многим».
Заметка
Реляционная база данных более гибкая, чем язык сетевого программирования, поскольку она не требует направления навигации; данные можно просматривать и извлекать любым возможным способом.
Сначала добавьте коллекцию участников в класс Event:
private Set participants = new HashSet();
public Set getParticipants() { return participants; }
public void setParticipants(Set participants) { this.participants = participants; }
Теперь отобразите эту сторону ассоциации в Event.hbm.xml
<set name="participants" table="PERSON_EVENT" inverse="true"> <key column="EVENT_ID"/> <many-to-many column="PERSON_ID" class="Person"/> </set>
Это нормальные Set
-отображения в обоих документах. Обратите внимание,
что имена столбцов в key
и «many-to-many
» меняются
в обоих документах. Наиболее важным дополнением здесь является атрибут inverse=«true»
в наборе элементов сопоставления коллекции Event
.
1.2.6. Работа двунаправленных связей
Во-первых, имейте в виду, что Hibernate не влияет на обычную семантику Java. Как мы создали
связь между Person
и Event
в однонаправленном примере? Вы добавляете
экземпляр Event
в коллекцию ссылок на события экземпляра Person
. Если
вы хотите сделать эту ссылку двунаправленной, вы должны сделать то же самое с другой стороны,
добавив ссылку Person
в коллекцию в Event
. Этот процесс
«установления связи с обеих сторон» абсолютно необходим с двунаправленными ссылками.
Многие разработчики программируют аккуратно и создают методы управления ссылками для правильной установки
обеих сторон (например, в Person
):
protected Set getEvents() { return events; }
protected void setEvents(Set events) { this.events = events; }
public void addToEvent(Event event) { this.getEvents().add(event); event.getParticipants().add(this); }
public void removeFromEvent(Event event) { this.getEvents().remove(event); event.getParticipants().remove(this); }
Методы get и set для коллекции теперь защищены. Это позволяет классам в одном и том же пакете и подклассах продолжать доступ к этим методам, но не позволяет всем другим напрямую изменять коллекции. Повторите шаги для коллекции на другой стороне.
Как насчет атрибута обратного (inverse)
отображения? Для вас и для Java двунаправленная
связь — это просто вопрос правильной установки ссылок с обеих сторон. Однако для Hibernate
недостаточно информации, чтобы правильно расположить инструкции SQL INSERT
и UPDATE
(чтобы избежать нарушения ограничений). Если одна сторона ассоциации обратная (inverse)
«говорит» Hibernate рассмотреть ее как зеркало (mirror) с другой стороны.
Это все, что необходимо для Hibernate чтобы разрешить любые проблемы, возникающие при преобразовании направленной
навигационной модели в схему базы данных SQL. Правила просты: все двунаправленные ассоциации нуждаются
в одной стороне как в обратной (inverse)
. В ассоциации «один-ко-многим»
это должно быть многостороннее, а в ассоциации «многие-ко-многим» вы можете
выбрать любую сторону.
1.3. Часть 3 - приложение Менеджер событий
Веб-приложение Hibernate использует Session
и Transaction
почти как автономное
приложение. Однако некоторые общие шаблоны полезны. Теперь вы можете написать EventManagerServlet
.
Этот сервлет может отображать все события, хранящиеся в базе данных, и предоставляет форму HTML
для ввода новых событий.
1.3.1. Написание основного сервлета
Сначала нам нужно создать наш основной сервлет обработки. Поскольку наш сервлет обрабатывает только HTTP-запросы
GET-запросы, мы реализуем только метод doGet()
:
package org.hibernate.tutorial.web;
// Imports public class EventManagerServlet extends HttpServlet {
protected void doGet( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { SimpleDateFormat dateFormatter = new SimpleDateFormat( "dd.MM.yyyy" );
try { // Begin unit of work HibernateUtil.getSessionFactory().getCurrentSession().beginTransaction();
// Process request and render page... // End unit of work HibernateUtil.getSessionFactory().getCurrentSession().getTransaction().commit(); } catch (Exception ex) { HibernateUtil.getSessionFactory().getCurrentSession().getTransaction().rollback(); if ( ServletException.class.isInstance( ex ) ) { throw ( ServletException ) ex; } else { throw new ServletException( ex ); } } } }
Сохраните код сервлета в src/main/java/org/hibernate/tutorial/web/EventManagerServlet.java
Образец, применяемый здесь, называется сессия-за-запрос (session-per-request). Когда запрос
попадает в сервлет, новый Hibernate Session
открывается через первый вызов
getCurrentSession()
в SessionFactory
. Затем запускается транзакция
с базой данных. Весь доступ к данным происходит внутри транзакции независимо от того,
читаются или записываются данные. Не используйте режим автоматической фиксации (auto-commit)
в приложениях.
Не используйте новый Hibernate Session
для каждой операции с базой данных.
Используйте один Hibernate Session
, охваченный (scoped) всем запросом. Используйте
getCurrentSession()
, чтобы он автоматически привязывался к текущему потоку Java.
Затем обрабатываются возможные действия запроса и визуализируется ответ HTML. Скоро мы перейдем к этой части.
Наконец, единица работы заканчивается, когда обработка и визуализация завершены. Если во время обработки
или визуализации возникли какие-либо проблемы, будет выброшено исключение и транзакция базы данных будет
откачена (rolled back). Это завершает шаблон сессия-за-запрос (session-per-request)
.
Вместо разграничения кода транзакций в каждом сервлете вы также можете написать фильтр сервлета.
См. Веб-сайт Hibernate и Wiki для получения дополнительной информации об этом шаблоне,
называемом Open Session in View. Вам понадобится это, как только вы подумаете о визуализации
вашего представления в JSP, а не в сервлете.
1.3.2. Обработка и визуализация
Теперь вы можете реализовать обработку запроса и рендеринг страницы.
// Write HTML header PrintWriter out = response.getWriter(); out.println("<html><head><title>Event Manager</title></head><body>");
// Handle actions if ( "store".equals(request.getParameter("action")) ) {
String eventTitle = request.getParameter("eventTitle"); String eventDate = request.getParameter("eventDate");
if ( "".equals(eventTitle) || "".equals(eventDate) ) { out.println("<b><i>Please enter event title and date.</i></b>"); } else { createAndStoreEvent(eventTitle, dateFormatter.parse(eventDate)); out.println("<b><i>Added event.</i></b>"); } }
// Print page printEventForm(out); listEvents(out, dateFormatter);
// Write HTML footer out.println("</body></html>"); out.flush(); out.close();
Этот стиль кодирования с сочетанием Java и HTML не будет масштабироваться в более сложном приложении. Имейте в виду, что мы только иллюстрируем основные концепции Hibernate в этом учебнике. Код печатает HTML-заголовок и нижний колонтитул. На этой странице печатается HTML-форма для записи события и список всех событий в базе данных. Первый метод тривиален и выводит только HTML:
private void printEventForm(PrintWriter out) { out.println("<h2>Add new event:</h2>"); out.println("<form>"); out.println("Title: <input name='eventTitle' length='50'/><br/>"); out.println("Date (e.g. 24.12.2009): <input name='eventDate' length='10'/><br/>"); out.println("<input type='submit' name='action' value='store'/>"); out.println("</form>"); }
Метод listEvents()
использует сессию Hibernate для привязки к текущему потоку для выполнения
запроса:
private void listEvents(PrintWriter out, SimpleDateFormat dateFormatter) {
List result = HibernateUtil.getSessionFactory() .getCurrentSession().createCriteria(Event.class).list(); if (result.size() > 0) { out.println("<h2>Events in database:</h2>"); out.println("<table border='1'>"); out.println("<tr>"); out.println("<th>Event title</th>"); out.println("<th>Event date</th>"); out.println("</tr>"); Iterator it = result.iterator(); while (it.hasNext()) { Event event = (Event) it.next(); out.println("<tr>"); out.println("<td>" + event.getTitle() + "</td>"); out.println("<td>" + dateFormatter.format(event.getDate()) + "</td>"); out.println("</tr>"); } out.println("</table>"); } }
Наконец, действие хранилища отправляется методу createAndStoreEvent()
,
который также использует сессию текущего потока:
protected void createAndStoreEvent(String title, Date theDate) { Event theEvent = new Event(); theEvent.setTitle(title); theEvent.setDate(theDate);
HibernateUtil.getSessionFactory() .getCurrentSession().save(theEvent); }
Сервлет завершен. Запрос к сервлету будет обрабатываться в течение одной сессии и транзакции.
Как ранее в автономном приложении, Hibernate может автоматически привязывать эти объекты к текущему
потоку выполнения. Это дает вам свободу сложить ваш код и получить доступ к SessionFactory
любым удобным вам способом. Обычно вы должны использовать более сложный дизайн и перемещать код доступа
к данным в объекты доступа к данным (шаблон DAO). См. Hibernate Wiki для получения дополнительных
примеров.
1.3.3. Развёртывание и тестирование
Чтобы развернуть это приложение для тестирования, мы должны создать Web ARchive (WAR). Сначала
мы должны определить дескриптор WAR как src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<servlet> <servlet-name>Event Manager</servlet-name> <servlet-class>org.hibernate.tutorial.web.EventManagerServlet</servlet-class> </servlet>
<servlet-mapping> <servlet-name>Event Manager</servlet-name> <url-pattern>/eventmanager</url-pattern> </servlet-mapping> </web-app>
Чтобы создать и развернуть пакет вызовите mvn
в каталоге проекта и скопируйте
файл hibernate-tutorial.war
в каталог webapps
сервера Tomcat.
Заметка
Реляционная база данных более гибкая, чем язык сетевого программирования, поскольку она не требует направления навигации; данные можно просматривать и извлекать любым возможным способом.
После развертывания запустите приложение в http://localhost:8080/hibernate-tutorial/eventmanager
.
Убедитесь, что вы смотрели журнал Tomcat, чтобы увидеть, что Hibernate инициализируется, когда первый запрос
попадает на ваш сервлет (вызывается статический инициализатор в HibernateUtil
)
и получает подробный вывод, если происходят какие-либо исключения.
1.4. Резюме
В этом учебном пособии рассмотрены основы написания простого автономного приложения Hibernate и небольшого веб-приложения. Дополнительные учебные материалы доступны на веб-сайте Hibernate.