14 сентября 2011 г.

Конспект "Hibernate reference manual"


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

Включение поддержки Hibernate в проект

Предположим, что ваше приложение использует систему сборки Maven. Тогда для включения поддержки библиотеки Hibernate нужно вставить следующие строки зависимости в ваш pom.xml:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
</dependency>
Класс для работы с Hibernate

Класс, реализующий сущность базы данных должен удовлетворять четырём правилам:
  1. Иметь конструктор по умолчанию (без аргументов).
  2. Иметь поле-идентификатор. (не обязательно)
  3. Быть не final-классом (не обязательно, но существенно для производительности)
  4. Иметь методы доступа для свойств.
Предположим, что у нас есть следующий класс, который мы хотим сохранить в базе данных:
public class SimpleClass {
    private Long id;
    private String text;
   
    public SimpleClass(){}

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

}
Свяжем этот класс с таблицой базы данных. Для этого используются файлы маппинга. В нашем случае он может иметь вид:
<hibernate-mapping package=”com.mycompany.app">
    <class name="SimpleClass" table="SIMPLECLASS">
        <id name="id" column="SIMPLE_ID">
            <generator class="native"/>
        </id>
        <property name=”text”/>
    </class>
</hibernate-mapping>
Все тэги этого файла интуитивно очевидны, кроме тэга generator, о котором я расскажу подробнее чуть позже.

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

Настройка соединения с БД

Существует несколько способов сказать Hibernate с какой БД мы будем работать и как к ней подключиться, из которых самым популярным является файл конфигурации, называемый hibernate.cfg.xml. Вот как он может выглядеть:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <property name="hibernate.dialect">org.hibernate.dialect.OracleDialect</property>
    <property name="hibernate.connection.driver_class">oracle.jdbc.OracleDriver</property>
    <property name="hibernate.connection.url">jdbc:oracle:thin:@//localhost </property>
    <property name="hibernate.connection.username">user</property>
    <property name="hibernate.connection.password">pass</property>
    <mapping resource="com/mycompany/app/simpleClass.hbm.xml"/>
  </session-factory>
</hibernate-configuration>
Работа с базой данных проходит в транзакциях в пределах сессии. Таким образом перед тем, как начать использовать Hibernate в своем приложении, необходимо создать фабрику сессий, обычно оборачиваемую в класс с именем HibernateUtil. Вот пример готового класса:
public class HibernateUtil {
    private static final SessionFactory sessionFactory = buildSessionFactory();
    private static SessionFactory buildSessionFactory() {
        try {
            // Create the SessionFactory from hibernate.cfg.xml
            return new Configuration().configure().buildSessionFactory();
        }
        catch (Throwable ex) {
            // Make sure you log the exception, as it might be swallowed
            System.err.println("Initial SessionFactory creation failed." + ex);
            throw new ExceptionInInitializerError(ex);
        }
    }
    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }
}
Базовые операции

Запись

Существует два способа сохранить класс в базе данных: используя методы persist() или save().
Первый не гарантирует создания уникальных идентификаторов на момент вызова. Метод полезен для длительных операций.

Метод save() возвращает уникальный идентификатор. Если для получения этого идентификатора должен быть вызван INSERT (т.е. используется генерация «identify», а не «sequence»), то INSERT вызывается сразу, вне зависимости в транзакции мы или нет. Это может создавать проблемы в длительных операциях, но мы можем явно указывать идентификатор перегруженным методом save(Class c, Long id).

Чтобы форсировать запись можно воспользоваться методом flush().

Пример записи:
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
SimpleClass sc = new SimpleClass();
sc.setText(“some text”);
session.save(sc);
session.getTransaction().commit();
Если база данных построена так, что некоторые поля инициализируются триггерами, то правильно сохранять объекты так:
sess.save(sc);
sess.flush(); //форсируем вызов SQL INSERT
sess.refresh(sc); //перечитываем объект из базы, триггеры уже отработали
По умолчанию метод commit() автоматически закрывает сессию, и при следующем обращении к getCurrentSession будет создана новая, так что делать close() или disconnect() не нужно.

Если неизвестно будет ли создан новый объект или обновлен старый то следует использовать метод saveOrUpdate().Следует помнить, что если вы не изменяете объекты из одной сессии в другой, то методы update(), saveOrUpdate() или merge() использовать не нужно.

Обычно update() или saveOrUpdate() использует в следующем сценарии:
  1. Приложение загружает данные в первой сессии
  2. Информация показывается пользователю, который её модифицирует
  3. Приложение обрабатывает эти изменения и сохраняет модификации вызывая update() в другой сессии.
Метод saveOrUpdate() делает следующее:
  • если объект уже существует в этой сессии – не делает ничего;
  • если другой объект в этой сессии имеет такой же идентификатор – генерит эксепшн;
  • если объект не имеет идентификатора – вызывает save();
  • если идентификатор объекта изменился – создает новый объект, т.е. вызывает save();
  • если объект обладает версией (свойствами <version>или <timestamp>), то создает новый объект;
  • в противном случае вызывается update().
Если объект загружен в одной сессии и сохраняется в другой, и потенциально существует вероятность того, что в базе он уже был изменен, то правильно его сохранять так:
// foo is an instance loaded by a previous Session
foo.setProperty("bar");
session = factory.openSession();
Transaction t = session.beginTransaction();
session.saveOrUpdate(foo); // Use merge() if "foo" might have been loaded already
t.commit();
session.close();
Hibernate сгенерит эксепшн во время сохранения, если конфликт был обнаружен. Вместо update() можно использовать lock() и использовать LockMode.READ если вы уверены, что объект не был изменен.

Чтение

Чтение из базы данных можно осуществлятся двумя методами: get() и load().
Пример чтения списка:
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
List result = session.createCriteria(SimpleClass.class).list();
session.getTransaction().commit();
Пример чтения записи по ключу:
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
SimpleClass sc = (SimpleClass) session.load(SimpleClass.class, Id);
session.getTransaction().commit();
Если объекта с данным идентификатором в базе не окажется, Hibernate сгенерирует эксепшн. Для решения этой ситуации можно воспользоваться методом get(Class c, Long id), к примеру:
SimpleClass sc = (SimpleClass) sess.get(SimpleClass.class, id);
if (sc ==null) {
    sc = new SimpleClass ();
    sess.save(sc, id);
}
return sc;
Для массива вместо метода list(), который возвращает сразу все данные, можно использовать итератор. Его использование аткуально для повышения производительности, если мы предпологаем, что объект, с которым мы собираемся работать, находится в кеше. Иначе такой запрос будет медленнее, чем list(). Пример использования из hibernate reference:
// fetch ids
Iterator iter = sess.createQuery("from eg.Qux q order by q.likeliness").iterate();
while ( iter.hasNext() ) {
    Qux qux = (Qux) iter.next(); // fetch the object
    // something we couldnt express in the query
    if ( qux.calculateComplicatedAlgorithm() ) {
        // delete the current instance
        iter.remove();
        // dont need to process the rest
        break;
    }
}
Удаление

Чтобы удалить объект из базы данных используется метод delete(). Нужно быть внимательным, потому что объекты удаляются без учёта ключей, т.е. связи нужно удалять вручную. Помните: сначла удаляем «детей», а уже потом их «родителей». :)

Критерий выборки

Существует два способа задать критерий выборки: HQL и критерион. Первый похож на native-SQL и вероятно больше понравится программистам, которые не хотят расставаться с написанием старых SQL-запросов вручную, второй более объектно-ориентирован.

Ниже приведу несколько примеров критерионов из hibernate reference:
Пример 1:
Criteria crit = session.createCriteria(Cat.class);
crit.add( Restrictions.eq( "color", eg.Color.BLACK ) );
crit.setMaxResults(10);
List cats = crit.list();
Пример 2:
List cats = sess.createCriteria(Cat.class)
    .add( Restrictions.like("name", "F%") )
    .createCriteria("kittens")
    .add( Restrictions.like("name", "F%") )
    .list();
Пример 3:
List cats = sess.createCriteria(Cat.class)
    .createAlias("kittens", "kt")
    .createAlias("mate", "mt")
    .add( Restrictions.eqProperty("kt.name", "mt.name") )
    .list();
Семантику выборки можно установить метдом setFetchMode(), например этот код создаст запрос с outer join:
List cats = sess.createCriteria(Cat.class)
    .add( Restrictions.like("name", "Fritz%") )
    .setFetchMode("mate", FetchMode.EAGER)
    .setFetchMode("kittens", FetchMode.EAGER)
    .list();
О параметре FetchMode мы поговорим чуть ниже.

Запросы «по примеру»

Можно создать класс, заполнить в нем некоторые поля и затем искать в базе те сущности, у которых эти заполненные поля исходного класса будут равны. Например:
Cat cat = new Cat();
cat.setSex('F');
cat.setColor(Color.BLACK);
List results = session.createCriteria(Cat.class)
    .add( Example.create(cat) )
    .list();
Идентификаторы и версии будут игнорированы. По умолчанию null valued свойства так же игнорируются. Можно задавать какие свойства игнорировать, например:
Example example = Example.create(cat)
    .excludeZeroes() //exclude zero valued properties
    .excludeProperty("color") //exclude the property named "color"
    .ignoreCase() //perform case insensitive string comparisons
    .enableLike(); //use like for string comparisons
List results = session.createCriteria(Cat.class)
    .add(example)
    .list();
Примеры можно использовать и в ассоциированных объектах:
List results = session.createCriteria(Cat.class)
    .add( Example.create(cat) )
    .createCriteria("mate")
    .add( Example.create( cat.getMate() ) )
    .list();
Проекции, аггрегация и группировка

Тут всё более, чем очевидно, только взгляните на этот пример:
List results = session.createCriteria(Cat.class)
    .setProjection( Projections.projectionList()
        .add( Projections.rowCount() )
        .add( Projections.avg("weight") )
        .add( Projections.max("weight") )
        .add( Projections.groupProperty("color") )
    )
    .list();
Или с группировкой:
List results = session.createCriteria(Cat.class)
    .setProjection( Projections.projectionList()
        .add( Projections.rowCount(), "catCountByColor" )
        .add( Projections.avg("weight"), "avgWeight" )
        .add( Projections.max("weight"), "maxWeight" )
        .add( Projections.groupProperty("color"), "color" )
    )
    .addOrder( Order.desc("catCountByColor") )
    .addOrder( Order.desc("avgWeight") )
    .list();
Общие сведения

Сессии

Существует три типа сессий (устанавливается свойством hibernate.current_session_context_class в hibernate.cfg.xml):
  • jta - org.hibernate.context.JTASessionContext: сессии отслеживаются и ограничены JTA транзакциями. Работа с ними происходит так же, как при использовании старого JTA-подхода;
  • thread - org.hibernate.context.ThreadLocalSessionContext:current сесии обрабатываются потоком;
  • managed -  org.hibernate.context.ManagedSessionContext: сесии обрабатываются потоком. В отличии от thread программист сам отвечает за связывание сесии. Для сесии не делается open, flush и close.
Первые два типа реализуют модель "one session - one database transaction", так же известную как session-per-request.

Если hibernate.current_session_context_class установлен в jta, то пользоваться сессиями так:
// Non-managed environment idiom
Session sess = factory.openSession();
Transaction tx = null;
try {
    tx = sess.beginTransaction();
    // do some work
    ...
    tx.commit();
}
catch (RuntimeException e) {
    if (tx != null) tx.rollback();
    throw e; // or display error message
}
finally {
    sess.close();
}
Если установлен в thread, то так:
// Non-managed environment idiom with getCurrentSession()
try {
    factory.getCurrentSession().beginTransaction();
    // do some work
    ...
    factory.getCurrentSession().getTransaction().commit();
}
catch (RuntimeException e) {
    factory.getCurrentSession().getTransaction().rollback();
    throw e; // or display error message
}
 Запросы можно формировать не в сессии, например:
DetachedCriteria query = DetachedCriteria.forClass(Cat.class)
    .add( Property.forName("sex").eq('F') );
Session session = ....;
Transaction txn = session.beginTransaction();
List results = query.getExecutableCriteria(session).setMaxResults(100).list();
txn.commit();
session.close();
Постраничная выборка

Постраничная выборка реализуется методами setFirstResult() и setMaxResult(), например так:
Query q = sess. createCriteria(SimpleClass.class);
q.setFirstResult(20);
q.setMaxResults(10);
List scList = q.list();
Методы equals() и hashCode()

Чтобы класс мог полноценно поддерживаться в many-to-one и many-to-many и использовать композитные ключи (в этом случае класс еще и должен реализовывать интерфейс Serializable) нужно реализовать в нем методы equals() и hashCode(). Самым простым вариантом реализации сравнения двух сущностей является проверка их идентификационных номеров, однако следует помнить, что при использовании автогенерации айдишников они будут недоступны при работе, если объект не был сохранен в базе. В документации по гибернейт рекомендуют сравнивать объекты основываясь на семантике бизнесс-логики. К примеру:
public class Cat {
    ...
    public boolean equals(Object other) {
        if (this == other) return true;
        if ( !(other instanceof Cat) ) return false;

        final Cat cat = (Cat) other;

        if ( !cat.getLitterId().equals( getLitterId() ) ) return false;
        if ( !cat.getMother().equals( getMother() ) ) return false;
        return true;
    }

    public int hashCode() {
        int result;
        result = getMother().hashCode();
        result = 29 * result + getLitterId();
        return result;
    }
}
Локировки

Сущестуют разные виды локировки:
  • LockMode.WRITE устанавливается автоматически, когда Hibernate обновляет или вставляет строку.
  • LockMode.UPGRADE устанавливается, когда программист хочет использовать конструкцию SELECT ... FOR UPDATE на базах данных, поддерживающих этот синтаксис
  • LockMode.UPGRADE_NOWAIT устанавливается, когда программист хочет использовать конструкцию SELECT ... FOR UPDATE NOWAIT используя Oracle.
  • LockMode.READ устанавливается автоматически, когда Hibernate читает данные из-под Repeatable Read или Serializable уровня изоляции.
  • LockMode.NONE означает отсутствие локировки.  Все объекты переключаются на этот вид локировки в конце транзакции. Объекты, ассоциированные с сессией вызовом update()или saveOrUpdate()так же стартуют с этим видом.
Если выбранный тип локировки не поддерживается базой данных – используется ближайший «по смыслу» к нему и эксепшн не генерируется. Это позволяет сохранять приложение портируемым.

Генератор идентификаторов

Чтобы избежать проблемы с одинаковыми идентификаторами при работе приложения в кластере нужно в hibernate-mapping конфиге изменить генератор (тэг generator). Могут быть следующие значения:
  • increment - генерирует идентификатор типа long, short или int, которые будет уникальным только в том случае, если другой процесс не добавляет запись в эту же таблицу в это же время.
  • identity - генерирует идентификатор типа long, short или int. Поддерживается в DB2, MySQL, MS SQL Server, Sybase и HypersonicSQL.
  • sequence - использует последовательности в DB2, PostgreSQL, Oracle, SAP DB, McKoi или генератор Interbase. Возвращает идентификатор типа long, short или int.
  • hilo - использует алгоритм hi/lo для генерации идентификаторов типа long, short или int. Алгоритм гарантирует генерацию идентификаторов, которые уникальны только в данной базе данных.
  • seqhilo - использует алгоритм hi/lo для генерации идентификаторов типа long, short или int учитывая последовательность базы данных.
  • uuid - использует для генерации идентификатора алгоритм 128-bit UUID. Идентификатор будет уникальным в пределах сети. UUID представляется строкой из 32 чисел.
  • guid - использует сгенерированую БД строку GUID в MS SQL Server и MySQL.
  • native - использует identity, sequence или hilo в завимисимости от типа БД, с которой работает приложение
  • assigned - позволяет приложению устанавливать идентификатор объекту, до вызова метода save(). Используется по умолчанию, если тег <generator> не указан.
  • select - получает первичный ключ, присвоенный триггером БД
  • foreign - использует идентификатор другого, связанного с данным объекта. Используется в <one-to-one> ассоциации первичных ключей.
  • sequence-identity - специализированный генератор идентификатора. Используется только с драйевром Oracle 10g дл JDK 1.4.
Hibernate exceptions

Если гибернейт бросает эксепшн, то нет никаких шансов восстановить работу сессии, потому что соответствие объектов в памяти и в базе более не гарантируется. Таким образом при любых эксепшенах вы должны закрыть сессию вызвав Session.close().

Версии

Для предотвращения конфликтов используется механизм версий. Например:
// foo is an instance loaded by a previous Session
session = factory.openSession();
Transaction t = session.beginTransaction();
int oldVersion = foo.getVersion();
session.load( foo, foo.getKey() ); // load the current state
if ( oldVersion != foo.getVersion() ) throw new StaleObjectStateException();
foo.setProperty("bar");
t.commit();
session.close();
Свойство version замапленно используя <version> и гибернейт будет его автоматически инкрементировать согласно выбранной политике (даты, таймштампы, итераторы) во время сохранения. Если механизм версий не используется, то в базе будет та информация, которую сохранили последней. Так делать не рекомендуется, потому что пользователь потенциально будет терять свою информацию без всяких предупреждений и сообщений об ошибках. Ручное сравнение версий так же возможно, однако нерационально для большинства приложений.

Производительность

Политика получения данных

При использовании связывания many-to-one по умолчанию используется выборка данных через select, а не через join. Переключается это свойством fetch в тэге many-to-one файла настройки связывания класса (*.hbm.xml).
Использование стратегии доступа по умолчанию к связанным сущностям через select потенциально добавляет проблем с производительностью. Их можно решив прописав что-то вроде:
<set name="permissions" fetch="join">
    <key column="userId"/>
    <one-to-many class="Permission"/>
</set>
или
<many-to-one name="mother" class="Cat" fetch="join"/>

Существуют следующие типа fetch'a:
  • Join fetching: hibernate получает ассоциированные объекты и коллекции одним SELECT используя OUTER JOIN
  • Select fetching: использует уточняющий SELECT чтобы получить ассоциированные объекты и коллекции. Если вы не установите lazy fetching определив lazy="false", уточняющий SELECT будет выполнен только когда вы запрашиваете доступ к ассоциированным объектам
  • Subselect fetching: поведение такое же, как у предыдущего типа, за тем исключением, что будут загружены ассоциации для все других коллекций, «родительским» для которых является сущность, которую вы загрузили первым SELECT’ом.
  • Batch fetching: оптимизированная стратегия вида select fetching. Получает группу сущностей или коллекций в одном SELECT’е.
Fetch определяет как будут получены объекты, а вот некоторые из стратегий, отвечающих за то когда они будут получены:
  • Immediate fetching: ассоцииации, коллекции или аттрибуты загружаются вместе с загрузкой родительской записи.
  • Lazy collection fetching:  коллекции загружаются тогда, когда над ними производятся операции. Этот тип используется по умолчанию для коллекций.
  • "Extra-lazy" collection fetching: отдельные элементы коллекций загружаются из БД. Hibernate пытается не загружать всю коллекцию целиком. Применяется для больших коллекций.
Стратегиям выборки посвящена почти вся 19.1 глава документа Hibernate Reference.

Кеш

Каждый объект, который был получен в сессии кешируется. Если сессия очень длинная или мы получаем слишком много объектов, то чтобы не получить OutOfMemoryException нужно периодически вызывать clear() и evict().

Таймаут для сессии

Существует механизм, позволяющий выставить таймаут для сессии (стоит учесть, что он не может быть использован в CMT бинах). Работает так:
//set transaction timeout to 3 seconds
sess.getTransaction().setTimeout(3);
sess.getTransaction().begin();

SQL “FOR UPDATE”

Можно использовать правило SQL “FOR UPDATE” для получения объектов, например:
Cat cat = (Cat) sess.get(Cat.class, id, LockMode.UPGRADE);
Если сервер базы данных не поддерживает операцию FOR UPDATE, то второй параметр метода get() будет проигнорирован. Это сделано для обеспечения переносимости кода между разными серверами баз данных.

HQL
Альтернативой использованию критерионов является язык HQL.
Примеры запросов на языке HQL из Hibernate Reference:
List cats = session.createQuery(
    "from Cat as cat where cat.birthdate < ?")
    .setDate(0, date)
    .list();
List mothers = session.createQuery(
    "select mother from Cat as cat join cat.mother as mother where cat.name = ?")
    .setString(0, name)
    .list();
List kittens = session.createQuery(
    "from Cat as cat where cat.mother = ?")
    .setEntity(0, pk)
    .list();
Cat mother = (Cat) session.createQuery(
    "select cat.mother from Cat as cat where cat = ?")
    .setEntity(0, izi)
    .uniqueResult();]]
Query mothersWithKittens = (Cat) session.createQuery(
    "select mother from Cat as mother left join fetch mother.kittens");
    Set uniqueMothers = new HashSet(mothersWithKittens.list());
Параметры для запросов можно подставлять в createQuery через символ «?» или именовать. Второе лучше, потому что:
  1. параметры можно перечислять в произвольном порядке;
  2. параметры могут использоваться несколько раз в запросе;
  3. параметры становятся самодокументируемыми.
Пример использования:
//named parameter (preferred)
Query q = sess.createQuery("from DomesticCat cat where cat.name = :name");
q.setString("name", "Fritz");
Iterator cats = q.iterate();

Или даже массивом:
//named parameter list
List names = new ArrayList();
names.add("Izi");
names.add("Fritz");
Query q = sess.createQuery("from DomesticCat cat where cat.name in (:namesList)");
q.setParameterList("namesList", names);
List cats = q.list();
Если запрос возврщает кортедж, то правильно его обрабатывать так:
Iterator kittensAndMothers = sess.createQuery(
    "select kitten, mother from Cat kitten join kitten.mother mother")
    .list()
    .iterator();
while ( kittensAndMothers.hasNext() ) {
    Object[] tuple = (Object[]) kittensAndMothers.next();
    Cat kitten = (Cat) tuple[0];
    Cat mother = (Cat) tuple[1];
    ....
}

5 комментариев:

Примечание. Отправлять комментарии могут только участники этого блога.