Глава 4. Долгоживущие Классы (Persistent Classes)

Долгоживущие классы (Persistent сlasses) - это классы приложения, реализующие сущности бизнес-задачи (например, Заказчик, Счет в приложениях электронной коммерции). Долгоживущий класс, как видно из названия, имеет временное и постоянное представления (постоянное представление - представление класса, использующееся для сохранения экземпляра этого класса, например, в базе данных; временное представление - экземпляр класса в приложении, т.е. объект времени выполнения в JVM).

Для того чтобы воспользоваться всеми приемуществами технологии Hibernate, рекомендуется использовать POJO (Plain Old Java Object) модель программирования при создании долгоживущих классов.

4.1. Простой POJO-пример

В качестве демонстрации приведем пример долгоживущего класса, стурктура которого является наиболее часто используемой при программировании.

package eg;
import java.util.Set;
import java.util.Date;

public class Cat {
private Long id; // идентификатор
private String name;
private Date birthdate;
private Cat mate;
private Set kittens
private Color color;
private char sex;
private float weight;

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

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

void setMate(Cat mate) {
this.mate = mate;
}
public Cat getMate() {
return mate;
}

void setBirthdate(Date date) {
birthdate = date;
}
public Date getBirthdate() {
return birthdate;
}
void setWeight(float weight) {
this.weight = weight;
}
public float getWeight() {
return weight;
}

public Color getColor() {
return color;
}
void setColor(Color color) {
this.color = color;
}
void setKittens(Set kittens) {
this.kittens = kittens;
}
public Set getKittens() {
return kittens;
}
// addKitten not needed by Hibernate
public void addKitten(Cat kitten) {
kittens.add(kitten);
}
void setSex(char sex) {
this.sex=sex;
}
public char getSex() {
return sex;
}
}

Следует соблюдать следующие четыре плавила:

4.1.1. Объявление методов доступа и изменения для долгоживущих полей

Класс Cat представляет методы доступа для всех сохраняемых полей. Многие другие ORM (Object Relational Model - объектно-реляционная модель) инструменты напрямую сохраняют переменные объекта долгоживущего класса. Мы думаем, что значительно лучше будет отделить детали реализации от механизма сохранения объекта. Hibernate работает со свойствами объектов в стиле JavaBeans и распознает имена методов в форме getFoo, isFoo и setFoo.

В отличие от объектов JavaBeans, методы доступа к свойствам объектов долгоживущих классов не обязаны быть публичными (public) - Hibernate может работать со свойствами, методы доступа к которым объявлены с модификаторами "по умолчанию", protected или private.

4.1.2. Создание конструктора по умолчанию

Класс Cat имеет неявный конструктор по умолчанию (конструктор без аргументов). Для корректной работы Hibernate все долгоживущие классы должны иметь такой конструктор (который может быть не public) для того, чтобы Hibernate мог создавать объекты этих классов с использованием Constructor.newInstance().

4.1.3. Определение свойства-идентификатора (необязательно)

Класс Cat имеет свойство под названием "id". Это свойство содержит значение столбца первичного ключа в таблице базы данных. Такое свойство может называться любым именем, и иметь любой примитивный тип, тип класса-обертки примитивного типа, а также типы java.lang.String и java.util.Date. Если ваши старые таблицы содержат составные ключи, вы можете использовать определенный пользователем класс со свойствами этих типов (см. ниже раздел, посвященный составным идентификаторам).

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

Более того, некоторая функциональность недоступна для классов, которые не имеют явно определенных идентификаторов. В частности:

  • Каскадное обновление (см. "Объекты жизненного цикла")

  • Session.saveOrUpdate()

Мы рекомендуем использовать в persistent-классах свойство-идентификатор с фиксированным названием, которое имеет непримитивный тип (чтобы оно могло принимать значение null)

4.1.4. Отдавать предпочтение не-final классам (необязательно)

Центральным элементом технологии Hibernate являются proxy-классы, которые опираются на возможность наследования от определенных пользователем долгоживущих классов или реализации соответствующих интерфейсов, задающих набор persistent-свойств через публичные set/get методы.

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

4.2. Реализация наследования

Подкласс также должен соблюдать первое и второе правила. Cвойство-идентификтор Наследуется у класса-родителя Cat.

package eg;

public class DomesticCat extends Cat {
private String name;

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

4.3. Реализация методов equals() и hashCode()

Вы должны переопределить методы equals() и hashCode() если предполагается использование объектов долгоживущих классов в качестве элементов коллекций или хэш-таблиц (например, в качестве элементов в наборе Set).

Это требование предъявляется только в случае, если объекты загружены в разных Hibernate-сессиях, т.к. Hibernate гарантирует JVM-идентичность ( a == b , реализация метода equals() по умолчанию) только в рамках одной сессии!

Фраза "гарантирует JVM-идентичность" означает, что если в какой-то момент через некоторую сессию был получен некоторый объект с идентификатором, например, "5", во в последующие моменты при повторном получении через ту же сессию этого объекта с этим идентификатором, сессия возвратит тот же экземпляр класса, если только приложение не удалит этот экземпляр из сессии самостоятельно (прим. пер.).

Даже если оба объекта a и b представляют одну и ту же запись в базе данных (т.е. имеют одно и то же значение первичного ключа в качестве идентификатора), мы не можем гарантировать, что они являются одним и тем же экземпляром вне в разных сессиях.

Наиболее очевидный способ реализации equals()/hashCode(), который сразу приходит в голову, это сравнивать значения идентификаторов объектов. Если значения совпадают, то оба объекта представляют одну и ту же запись в базе данных, и таким образом, они эквивалентны (то есть, если оба эти объекта будут добавлены в коллекцию Set, то только один из них в ней будет присутствовать). К сожалению, мы не можем использовать этот подход. Hibernate присваивает значения идентификаторам только уже сохраненных объектов - новые объекты не имеют этого идентификатора до тех пор, пока они не будут сохранены в базу данных! Мы рекомендуем реализовывать методы equals() и hashCode() используя семантику значений, хранящихся в объекте persistent-класса (т.н. Business key equality - эквивалентность по бизнес-ключу).

Эквивалентность по бизнес-ключу означает, что метод equals() сравнивает только те свойства, которые идентифицируют объект в реальном мире (составляющие т.н. потенциальный естественный ключ).

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 (!getName().equals(cat.getName())) return false;
if (!getBirthday().equals(cat.getBirthday())) return false;

return true;
}

public int hashCode() {
int result;
result = getName().hashCode();
result = 29 * result + getBirthday().hashCode();
return result;
}

}

Помните, что наш потенциальный ключ (в данном случае это имя и день рождения) должен лишь подходить для конкретной операции сравнения (возможно, что только в одном-единственном случае - при использовании в методе equals()), т.к. нам не нужна реализация требований стабильности, обычно предъявляемых к первичным ключам.

4.4. Callback-методы жизненного цикла

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

В добавление к Lifecycle интерфейс Interceptor предоставляет более независимую от бизнес-логики альтернативу.

public interface Lifecycle {
public boolean onSave(Session s) throws CallbackException; (1)
public boolean onUpdate(Session s) throws CallbackException; (2)
public boolean onDelete(Session s) throws CallbackException; (3)
public void onLoad(Session s, Serializable id);(4)
}
(1)

onSave - вызывается непосредственно перед сохранением объекта в базу данных

(2)

onUpdate - вызывается непосредственно перед обновлением объекта (когда объект передается в метод Session.update())

(3)

onDelete - вызывается непосредственно перед удалением объекта

(4)

onLoad - вызывается сразу после загрузки объекта

Методы onSave(), onDelete() и onUpdate() могут использоваться для каскадного сохранения и удаления зависимых объектов, что эквивалентно объявлению каскадных операций в mapping-файле. Метод onLoad() может использоваться для инициализации transient-свойств (т.е. свойств, которые не записываются в базу при сохранении объекта, прим. пер.) на основе доступных долгоживущих свойств. Этот метод не может использоваться для загрузки зависимых объектов, т.к. Hibernate запрещает вызывать методы сессии из onLoad(). Методы onLoad(), onSave() и onUpdate() могут также сохранять ссылку на текущую сессию для дальнейшего использования.

Имейте в виду, что метод onUpdate() вызывается только тогда, когда объект передается в Session.update(), а не при изменении свойств объекта.

Если методы onSave(), onUpdate() или onDelete() возвращают true, то операция прерывается без ошибки. Если при исполнении этих методов пробрасывается исключение CallbackException, то операция прерывается, а исключение передается обратно в приложение.

Если не используется родная генерация ключей (native key generation), то метод onSave() вызывается после того, как объекту назначается идентификатор.

4.5. Проверочные callback-вызовы

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

public interface Validatable {
public void validate() throws ValidationFailure;
}

Объект должен бросить исключение ValidationFailure если какое-то из условий сохранения нарушено. Объект класса, реализующего интерфейс Validatable не должен менять свое состояние в процессе проверки (validate()).

В отличие от callback-методов интерфейса Lifecycle, время вызова метода validate() не определено. Бизнес-логика приложения не должна полагаться на вызовы validate().

4.6. Использование XDOclet описания

В следующей главе мы покажем, как задается соответствие между классами и структурой базы данных с помощью mapping-файлов в простом, удобном для чтения XML формате. Многие пользователи Hibernate предпочитают определять эти соответствия прямо в исходном коде классов, используя XDoclet @hibernate.tags. В этом документе мы не будем описывать этот подход, т.к., строго говоря, это часть XDoclet документации. Тем не менее, ниже мы приведем пример класса Cat с XDoclet описанием.

package eg;
import java.util.Set;
import java.util.Date;

/**
 * @hibernate.class
 *table="CATS"
 */
public class Cat {
private Long id; // идентификатор
private Date birthdate;
private Cat mate;
private Set kittens
private Color color;
private char sex;
private float weight;

/**
 * @hibernate.id
 *generator-class="native"
 *column="CAT_ID"
 */
public Long getId() {
return id;
}
private void setId(Long id) {
this.id=id;
}

/**
 * @hibernate.many-to-one
 *column="MATE_ID"
 */
public Cat getMate() {
return mate;
}
void setMate(Cat mate) {
this.mate = mate;
}

/**
 * @hibernate.property
 *column="BIRTH_DATE"
 */
public Date getBirthdate() {
return birthdate;
}
void setBirthdate(Date date) {
birthdate = date;
}
/**
 * @hibernate.property
 *column="WEIGHT"
 */
public float getWeight() {
return weight;
}
void setWeight(float weight) {
this.weight = weight;
}

/**
 * @hibernate.property
 *column="COLOR"
 *not-null="true"
 */
public Color getColor() {
return color;
}
void setColor(Color color) {
this.color = color;
}
/**
 * @hibernate.set
 *lazy="true"
 *order-by="BIRTH_DATE"
 * @hibernate.collection-key
 *column="PARENT_ID"
 * @hibernate.collection-one-to-many
 */
public Set getKittens() {
return kittens;
}
void setKittens(Set kittens) {
this.kittens = kittens;
}
// addKitten not needed by Hibernate
public void addKitten(Cat kitten) {
kittens.add(kitten);
}

/**
 * @hibernate.property
 *column="SEX"
 *not-null="true"
 *update="false"
 */
public char getSex() {
return sex;
}
void setSex(char sex) {
this.sex=sex;
}
}