Глава 1. Быстрый старт с использованием Tomcat

1.1. Начнем с Hibernate

Данный учебный материал разъясняет процесс установки Hibernate 2.1 с использованием Apache Tomcat servlet контейнера для web-приложений. Hibernate прекрасно работает в управляемом окружение (managed environment) со всеми основными J2EE серверами приложений, а также и в самостоятельных Java приложениях (standalone Java applications). В данном материале используется база данных PostgreSQL 7.3, поддержка других баз данных настраивается всего-навсего изменением настройки используемого диалекта SQL в Hibernate.

В начале мы должны скопировать все необходимые библиотеки в установленный Tomcat. Мы используем отдельный web-контекст (webapps/quickstart) для данного учебного материала. Таким образом мы должны рассмотреть глобальный путь поиска библиотек (global classpath) TOMCAT/common/lib и путь поиска библиотек в нашем web-контексте (context classpath) webapps/quickstart/WEB-INF/lib (для JAR-файлов), webapps/quickstart/WEB-INF/classes (для классов нашего приложения). Будем ссылаться на оба пути поиска библиотек и классов, как на глобальный, так и на конекстный (global classpath, context classpath), соответсвующие загрузчики классов будут называться глобальный и контекстный загрузчики (global classloader, context classloader).

Теперь скопируем библиотеки в оба пути поиска (classpath):

  1. Скопируйте JDBC драйвер использумой базы данных в global classpath. Это требование продиктовано программным обеспечением 'DBCP пул соединений' (DBCP connection pool), которое поставляется вместе с Tomcat. Hibernate использует JDBC соединения (JDBC connections) для вызова SQL запросов к базе данных, поэтому вы либо должны предоставить настроенный пул JDBC соединений (JDBC connection pool), либо настроить Hibernate для использования одного из напрямую поддерживаемых пулов встроенных в Hibernate (C3P0, Proxool). Для данного материала, скопируйте библиотеку pg73jdbc3.jar (для PostgreSQL 7.3 и JDK 1.4) в global classpath. Если вы хотите использовать какую-нибудь другую базу данных, то просто скопируйте соответсвующий ей JDBC драйвер.

  2. Никогда не копируйте ничего больше в global classpath в Tomcat, иначе вы получите проблемы с использованием различных инструментов, включая Log4j, commons-logging и т.д. Всегда используйте context classpath для каждого web-приложения, т.е. копируйте библиотеки в WEB-INF/lib а ваши классы, настройки и файлы Java-свойств (properties) в WEB-INF/classes. По умолчанию обе эти директории находятся в context classpath и доступны для context classloader'а.

  3. Hibernate упакован как JAR-библиотека. Файл hibernate2.jar должен быть скопирован в context classpath вместе с остальными классами приложения. При работе, Hibernate использует ряд третьесторонних библиотек, которые поставляются вместе с дистрибутивом Hibernate в каталоге lib/; смотри Таблица 1.1, « Третьесторонние библиотеки использумые в Hibernate ». Скопируйте требуемые третьесторонние библиотеки в context classpath web-приложения.

Таблица 1.1. Третьесторонние библиотеки использумые в Hibernate

Библиотека Описание
dom4j (требуется) Hibernate использует dom4j для разбора файлов XML-настроек и XML-мэппинга метаданных.
CGLIB (требуется) Hibernate использует данную библиотеку генерации кода для расширения классов во время исполнения (в сочетании с Java reflection).
Commons Collections, Commons Logging (требуется) Hibernate использует различные вспомогательные библиотеки из проекта Apache Jakarta Commons.
ODMG4 (требуется) Hibernate предоставляет опциональный согласованный с ODMG интефейс управления постоянством (ODMG compliant persistence manager interface). Даже если вы не намерены использовать ODMG API, данный интефейс необходим, если вы собираетесь мэпить коллекции. В данном учебном материале мы не будем мэпить коллекции, но, тем не менее, скопировать данный JAR-файл -- хорошая идея.
EHCache (требуется) Hibernate может использовать различные кэш провайдеры (cache providers) для кэша второго уровня (second-level cache). EHCache -- кэш провайдер, используемый по умолчанию, если кэш провайдер не был установлен при конфигурации.
Log4j (опционально) Hibernate использует Commons Logging API, который, в свою очередь, может использовать Log4j нижележащего механизма логирования. Если Log4j доступен в context classpath (webapps/quickstart/WEB-INF/lib), Common Logging будет использовать Log4j и конфигурационный файл log4j.properties, расположенный в webapps/quickstart/WEB-INF/classes Пример файла настроек для Log4j входит в состав дистрибутива Hibernate. Таким образом, скопируйте log4j.jar и конфигурационный файл src/log4j.properties в context classpath, если вы хотите видеть, что происходит за сценой.
Требуется или нет? Взгляните на файл lib/README.txt из дистрибутива Hibernate. Это самый последний (up-to-date) список третьесторонних библиотек, поставляемых с Hibernate. Здесь перечислены все требуемые и опционалные библиотеки.

Теперь настроим общий для Hibernate и Tomcat пул соединений с базой данных (shared database connection pool). Это означает, что Tomcat предоставляет пул JDBC соединений (pooled JDBC connections) (используя встроенный DBCP пул), Hibernate запрашивает данные соединения через интерфейс JNDI. Tomcat привязывает (binds) пул соединений к JNDI, для этого добавим объявление ресурса (resource declaration) в главный конфигурационный файл Tomcat, TOMCAT/conf/server.xml:

<Context path="/quickstart" docBase="quickstart">
<Resource name="jdbc/quickstart" scope="Shareable" type="javax.sql.DataSource"/>
<ResourceParams name="jdbc/quickstart">
<parameter>
<name>factory</name>
<value>org.apache.commons.dbcp.BasicDataSourceFactory</value>
</parameter>

<!-- DBCP database connection settings -->
<parameter>
<name>url</name>
<value>jdbc:postgresql://localhost/quickstart</value>
</parameter>
<parameter>
<name>driverClassName</name><value>org.postgresql.Driver</value>
</parameter>
<parameter>
<name>username</name>
<value>quickstart</value>
</parameter>
<parameter>
<name>password</name>
<value>secret</value>
</parameter>

<!-- DBCP connection pooling options -->
<parameter>
<name>maxWait</name>
<value>3000</value>
</parameter>
<parameter>
<name>maxIdle</name>
<value>100</value>
</parameter>
<parameter>
<name>maxActive</name>
<value>10</value>
</parameter>
</ResourceParams>
</Context>

Контекст, сконфигурированный в данном примере, назван quickstart, и размещен в каталоге TOMCAT/webapp/quickstart. Для доступа к любому из сервлетов (servlets), в браузере наберите следующий путь http://localhost:8080/quickstart (добавив имя сервлета, сконфигурированного в вашем web.xml). Теперь вы можете продолжить и создать простой сервлет с пустым методом process().

С данными настройками Tomcat использует DBCP пул соединений и предоставляет объекты JDBC Connection из этого пула через JNDI имя java:comp/env/jdbc/quickstart. Если вы испытываете проблемы с настройкой и работой пула соединений, обратитесь к документации Tomcat. Если вы получили сообщения с исключительной ситуацией от JDBC драйвера (JDBC driver exception messages), попытайтесь сперва настроить пул JDBC соединений без подключения его в Hibernate. Учебный материал "Tomcat & JDBC" доступен в Web.

Следующим шагом настроим Hibernate для использования пула соединений привязанного (bound) через JNDI. Используем настройку Hibernate через XML-файл. Основной способ настройки, через файл Java-свойств (properties), эквивалентен, но не предоставляет дополнительных расширенных опций. Мы будем использовать настройку через XML, потому что обычно она более удобна. Конфигурационный XML-файл должен быть размещен в context classpath (WEB-INF/classes), под именем hibernate.cfg.xml:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration
PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">

<hibernate-configuration>

<session-factory>

<property name="connection.datasource">java:comp/env/jdbc/quickstart</property>
<property name="show_sql">false</property>
<property name="dialect">net.sf.hibernate.dialect.PostgreSQLDialect</property>

<!-- Mapping files -->
<mapping resource="Cat.hbm.xml"/>

</session-factory>

</hibernate-configuration>

Мы отключили логирование SQL команд, настроили используемый диалект SQL и указали, как Hibernate должен получать JDBC соединения (объявив JNDI имя, к которому Tomcat привязал пул соединений). Диалект (dialect) является обязательным конфигурационным параметром, базы данных отличаются своей интерпретацией SQL "стандарта" (интерпретируют его по своему). Hibernate берет заботу об этих отличиях на себя и поставляется с диалектами для всех основных коммерческих баз данных и баз данных с открытым исходным кодом.

SessionFactory -- это концепция единого хранилища данных (single datastore). При создании нескольких конфигурационных XML-файлов будут использоваться несколько баз данных, и будут созданы несколько объектов (по одному для каждой базы данных) Configuration и SessionFactory.

Самый последний элемент в hibernate.cfg.xml, объявляет Cat.hbm.xml как имя файла XML-мэппинга для долгоживущего класса (persistent class) Cat. Данный файл содержит метаданные, описывающие для Hibernate, как следует сохранить данные из POJO класса (POJO class) в таблицу базы данных (или в несколько таблиц). Вскоре мы вернемся к этому файлу. Сначала давайте создадим POJO класс, а затем объявим метаданные в файле мэппинга (mapping metadata) для созданного POJO класса.

Мэппинг

От английского слова "mapping" -- "нанесение на карту; вычерчивание карт; картография; топографическая съемка". В данном контексте означает процесс преобразования/отображения объектной модели данных (POJO) в реляционную (relational) модель и, наоборот, преобразование реляционной модели в объектную по единым правилам, описанным в файле XML-мэппинга (XML mapping file). Другими словами, термин мэппинг означает преобразование Java объектов в строки таблиц базы данных.

1.2. Первый долгоживущий класс (persistent class)

Наилучшая для Hibernate модель данных долгоживущих классов -- это Plain Old Java Objects (POJOs, иногда также называемая Plain Ordinary Java Objects). POJO очень похож на JavaBean со свойствами (properties), доступными через getter- и setter-методы, которые прячут внутреннее представление класса от публичного доступа:

package net.sf.hibernate.examples.quickstart;

public class Cat {

private String id;
private String name;
private char sex;
private float weight;

public Cat() {
}

public String getId() {
return id;
}

private void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public char getSex() {
return sex;
}

public void setSex(char sex) {
this.sex = sex;
}

public float getWeight() {
return weight;
}

public void setWeight(float weight) {
this.weight = weight;
}

}

Hibernate не ограничен использованием каких-то определенных типов, все Java типы и примитивы (такие как String, char и Date) могут быть использованы при мэппинге, включая классы из Java Collections Framework. Их можно использовать в мэппиге как значения (values), коллекции значений (collections of values) или ассоциации (associations) к другим сущностям (entities). Специальное свойство (property) id представляет идентификатор из базы данных (primary key) данного класса, данное свойство настоятельно рекомендовано для использования с сущностями такими как Cat. Hibernate мог бы использовать идентификаторы только для своих внутренних нужд, но скрывая их, мы бы потеряли часть гибкости в архитектуре нашего приложения.

Мы не должны реализовывать никаких специальных интерфейсов для долгоживущих классов (persistent classes), также не должны наследовать наши долгоживущие классы от специального базового класса. К тому же, Hibernate не использует никаких дополнительных обработок исходного кода и байт-кода приложения во время сборки, он полность полагается на рефлексию (reflection) и расширение классов во время исполнения (runtime class enhancement), используя библиотеку CGLIB. Таким образом, не имея никаких зависимостей от Hibernate в нашем POJO классе, мы можем отобразить/замэпить наш класс на таблицу базы данных.

1.3. Замэпим кота

Файл мэппинга Cat.hbm.xml, содержит метеданные необходимые для отображения/мэппинга объектной структуры данных в реляционную структуру (object/relational mapping). Метаданные содержат объявления долгоживущих классов и отображение/мэппинг свойств классов на колонки и внешние ключи таблиц базы данных.

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

<hibernate-mapping>

<class name="net.sf.hibernate.examples.quickstart.Cat" table="CAT">

<!-- A 32 hex character is our surrogate key. It's automatically
generated by Hibernate with the UUID pattern. -->
<id name="id" type="string" unsaved-value="null" >
<column name="CAT_ID" sql-type="char(32)" not-null="true"/>
<generator class="uuid.hex"/>
</id>

<!-- A cat has to have a name, but it shouldn' be too long. -->
<property name="name">
<column name="NAME" length="16" not-null="true"/>
</property>

<property name="sex"/>

<property name="weight"/>

</class>

</hibernate-mapping>

Каждый долгоживущий (persistent) класс должен иметь идентификатор (на самом деле, только классы представляющие сущности (entities); зависимые объекты, замэпленные как компоненты (components) могут не иметь идентификатора). Идентификтор используется для того чтобы различать долгоживущие объекты: два кота идентичны если выражение catA.getId().equals(catB.getId()) является истинным, данная концепция называется идентичность средствами базы данных (database identity). Hibernate поставляется с несколькими генераторами идентификторов для различных сценариев использования (включая родные (native) генераторы, генераторы для использования database sequence, hi/lo идентификационных таблиц и генераторы для использования идентификторов присвоенных приложением). В данном примере, мы используем UUID-генератор (рекомендован только для тестовых целей, целочисленный суррогатный ключ, сгенерированный базой данных, является наиболее предпочтительным). Также указываем колонку CAT_ID таблицы CAT, для использования под сгенерированный идентификатор (данная колонка используется как первичный ключ таблицы).

Все свойства класса Cat замэплены/отображаются в одну таблицу. Свойство name мы замэпили с использованием явного указания имени колоки таблицы базы данных. Данная техника особенно важна при схеме данных (SQL DDL выражения), автоматически сгенерированной из мэппингов Hibernate с ипользованием инструмента SchemaExport. Все остальные свойства замэплены используя настройки Hibernate по умолчания, это то, что в основном вам и надо. Таблица CAT в базе данных выглядит как:

 Колонка |Тип| Модификатор
---------+-----------------------+------------
 cat_id| character(32) | not null
 name| character varying(16) | not null
 sex | character(1)|
 weight| real|
Indexes: cat_pkey primary key btree (cat_id)

Теперь вы должны вручную создать эту таблицу в базе данных, позже прочитайте Глава 15, Toolset Guide, если желаете автоматизировать данный процесс с использованием инструмента SchemaExport. Данный инструмент умеет создавать полный SQL DDL скрипт, включая опредение таблиц, установка ограничений на типы (???) (custom column type constraints), установка ограничений уникальности (unique constraints) и установка индексов.

1.4. Поиграем с котами

Теперь мы готовы стартовать Hibernate Session. Session -- это интерфейс управления постоянством объектов (persistence manager), он используется для сохранения в базу данных и восстановления из нее объектов класса Cat. Но с начала мы должны получить экземпляр класса Session из SessionFactory, Session в Hibernate является единицей-работы (unit-of-work):

SessionFactory sessionFactory =
new Configuration().configure().buildSessionFactory();

Один экземпляр SessionFactory ответсвеннен за одну базу данных и может использовать только один конфигурационный XML-файл (hibernate.cfg.xml). До того как вы создали экземпляр SessionFactory, вы можете установить другие свойства (и даже изменить метаданные мэппинга) путем доступа к Configuration. Т.к. SessionFactory является неизменяемым (immutable), устанавливать дополнительные свойства и менять метаданные через Configuration можно только до создания экземляра SessionFactory. Где мы создадим экземпляр SessionFactory и каким образом мы будем получать к нему доступ из на шего приложения?

Обычно экземпляр SessionFactory создается только один раз, например при старте приложения, с использованием load-on-startup сервлета (Java Servlet). Это также означает что вы не должны хранить его в переменной-члене вашего сервлета, для этого должно использоваться более подходящее место. Для того чтобы мы могли легко получать доступ к экземляру SessionFactory, нам необходим своего рода Singleton. Подход демонстрируемый далее, решает обе проблемы: конфигурация и легкий удобный доступ к SessionFactory.

Реализуем вспомогательный класс HibernateUtil:

import net.sf.hibernate.*;
import net.sf.hibernate.cfg.*;

public class HibernateUtil {

private static final SessionFactory sessionFactory;

static {
try {
// Create the SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (HibernateException ex) {
throw new RuntimeException("Configuration problem: " + ex.getMessage(), ex);
}
}

public static final ThreadLocal session = new ThreadLocal();

public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// Open a new Session, if this Thread has none yet
if (s == null) {
s = sessionFactory.openSession();
session.set(s);
}
return s;
}

public static void closeSession() throws HibernateException {
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
}

Данный класс не только заботится о SessionFactory, используя статический член класса, но также имеет ThreadLocal член, для хранения Session текущего выполняемого потока. Перед тем как начать использовать данный вспомогательный класс, удостоверьтесь, что вы понимаете Java-концепцию thread-local переменных.

SessionFactory является потоко-безопасным классом (threadsafe class), несколько потоков могут иметь одновременный доступ к одному экземляру и запрашивать сессии (экземпляры Session). Экземляр класса Session, не является потоко-безопасным (non-threadsafe), он предсавляет отдельную единицу-работы (single unit-of-work) с базой данных. Сессии открываются экземляром класса SessionFactory и закрываются когда вся работа с базой данных завершена:

Session session = HibernateUtil.currentSession();

Transaction tx= session.beginTransaction();

Cat princess = new Cat();
princess.setName("Princess");
princess.setSex('F');
princess.setWeight(7.4f);

session.save(princess);
tx.commit();

HibernateUtil.closeSession();

В сессии, все операции происходят внутри транзакции, транзакция изолирует операции с базой данных (даже операции только на чтение) от параллельных операций производимых из других транзакций. Мы используем Hibernates Transaction API для абстрагирования от нежележащей транзакционной стратегии, в нашем влучае от JDBC транзакций. Это позволяет помещать наш код в окружение с транзакциями управляемыми контейнером (container-managed transactions, JTA) без каких-либо изменений. Пожалуйста обратите внимание, что пример сверху не обрабатывает исключительные ситуации.

Также обратите внимание, что вы можете вызывать HibernateUtil.currentSession(); столько раз сколько хотите, вы всегда получите текущую сессию для потока в котором работает ваш код. Вы должны удостовериться что сессия закрыта после того как завершена ваша единица-работы (unit-of-work), либо в коде сервлета, либо в фильтре (servlet filter), до того как был послан HTTP отклик (HTTP response). Приятный побочный эффект от закрытия сессии перед самым отправлением HTTP отклика, -- это "ленивая" инициализация POJO объектов: т.к. при генерации представления (HTML), сессия все еще открыта, Hibernate может загружать неинициализированные объекты и инициализировать их по мере обращения к ним.

Hibernate имеет различные способы восстановления объектов из базы данных. Наиболее гибкий -- это использование Hibernate Query Language (HQL). HQL -- это легкий в изучении и мощный объектно-ориентированныый язык, являющийся расширением SQL:

Transaction tx = session.beginTransaction();

Query query = session.createQuery("select c from Cat as c where c.sex = :sex");
query.setCharacter("sex", 'F');
for (Iterator it = query.iterate(); it.hasNext();) {
Cat cat = (Cat) it.next();
out.println("Female Cat: " + cat.getName() );
}

tx.commit();

Hibernate также предлагает объектно-ориентированный query by criteria API (построение запроса по критерию), данный API использует бесопасные типы для построения запросов. Ну и конечно Hibernate использует объекты PreparedStatement и привязку параметров (parameter binding) для всех SQL-запросов к базе данных. В редких случаях, вы можете использовать низкоуровневые SQL запросы напрямую или получить из Hibernate Session, простой JDBC Connection

1.5. Заключение

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

Помните, что Hibernate, как слой доступа к данным (data access layer), тесно интегрирован в ваше приложение. Обычно все остальные слои зависимы от механизма сохранения объектов (persistence mechanism). Убедитесь что вы абсолютно понимаете этот дизайн.