Глава 4. Постоянные классы

Оглавление
  1. 4.1. Пример простого POJO
    1. 4.1.1. Реализация конструктора без аргументов
    2. 4.1.2. Предоставление свойства идентификатора
    3. 4.1.3. Предпочтение "не-конечным" классам (частично необязательно)
    4. 4.1.4. Объявление аксессоров и мутаторов для постоянных полей (необязательно)
  2. 4.2. Реализация наследования
  3. 4.3. Реализация equals() и hashCode()
  4. 4.4. Динамические модели
  5. 4.5. Tuplizers (Кортежизаторы)
  6. 4.6. EntityNameResolvers

Постоянными классами (Persistent classes) являются классы в приложении, которые реализуют сущности бизнес-задач (например "Клиент" и "Заказ" в приложении электронной коммерции). Термин «постоянный» (persistent) здесь означает, что классы могут сохранять своё состояние, а не то, что они находятся в постоянном состоянии (см. Параграф 11.1 «Состояние объекта Hibernate»).

Hibernate лучше всего работает если эти классы следуют нескольким простым правилам, также известным как модель программирования Plain Old Java Object (POJO). Однако ни одно из этих правил не является жёстким требованием. В самом деле, Hibernate имеет очень малое отношение к природе ваших постоянных объектов. Вы можете выразить модель домена другими способами (например, используя деревья экземпляров java.util.Map).

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

Пример 4.1. Простой POJO, представляющий кошку.

package eg;
import java.util.Set;
import java.util.Date;
public class Cat { private Long id; // идентификатор private Date birthdate; private Color color; private char sex; private float weight; private int litterId; private Cat mother; private Set kittens = new HashSet();
private void setId(Long id) { this.xml:id=id; } public Long getId() { return id; }
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 setSex(char sex) { this.sex=sex; } public char getSex() { return sex; }
void setLitterId(int id) { this.litterId = id; } public int getLitterId() { return litterId; }
void setMother(Cat mother) { this.mother = mother; } public Cat getMother() { return mother; }
void setKittens(Set kittens) { this.kittens = kittens; } public Set getKittens() { return kittens; }
// addKitten не нужен Hibernate public void addKitten(Cat kitten) { kitten.setMother(this); kitten.setLitterId(kittens.size()); kittens.add(kitten); } }

Четыре основных правила постоянных классов более подробно рассматриваются в следующих разделах.

4.1.1. Реализация конструктора без аргументов

Cat имеет конструктор без аргументов. Все постоянные классы должны иметь конструктор по умолчанию (который может быть private), чтобы Hibernate мог создавать их с помощью java.lang.reflect.Constructor.newInstance(). Рекомендуется, чтобы этот конструктор имел модификатор доступа как минимум package, чтобы генерация прокси во время выполнения работала должным образом.

4.1.2. Предоставление свойства идентификатора

Заметка

Исторически это считалось опцией. Пока все еще не применяется (пока), это следует рассматривать как устаревшую функцию, так как она будет полностью обязана предоставить свойство идентификатора в предстоящей версии.

Cat имеет свойство с именем id. Это свойство отображается на столбец первичных ключей таблицы в базе данных. Тип свойства идентификатора может быть любым «базовым» типом (см. раздел «Базовые типы»). Для получения информации об отображении составных (многоколоночных) идентификаторов см. раздел 9.4. «Компоненты как составные идентификаторы».

Заметка

Идентификаторы необязательно должны идентифицировать столбец (столбцы) в базе данных, физически определяемые как первичный ключ. Они должны просто идентифицировать столбцы, которые могут использоваться для уникальной идентификации записей в таблице.

Мы рекомендуем декларировать свойства идентификатора с одинаковым именем в постоянных классах и использовать тип с нулевым значением (nullable) (т. е. не примитивный).

4.1.3. Предпочтение "не-конечным" (non-final) классам (частично необязательно)

Центральная функция Hibernate - proxies (ленивая загрузка) - зависит от постоянного класса, являющегося либо нефинальным, либо реализующего интерфейс, который объявляет все методы как public. Вы можете сохранить (persist) final-классы, которые не реализуют интерфейс с Hibernate. Однако вы не сможете использовать прокси для ленивой выборки ассоциации, которая в конечном итоге ограничит ваши возможности настройки производительности. Чтобы сохранить (persist) final-класс, который не реализует «полный» интерфейс, вы должны отключить генерацию прокси. См. Пример 4.2. «Отключение прокси в hbm.xml» и Пример 4.3. «Отключение прокси в аннотациях».

Пример 4.2. Отключение прокси в hbm.xml

<class name="Cat" lazy="false"...>...</class>

Пример 4.3. Отключение прокси в аннотациях

@Entity @Proxy(lazy=false) public class Cat { ... }

Если final-класс реализует правильный интерфейс, вы можете по-другому сказать Hibernate использовать интерфейс вместо генерации прокси. См. Пример 4.4. «Проксирование интерфейса в hbm.xml» и Пример 4.5. «Проксирование интерфейса в аннотациях».

Пример 4.4. Проксирование интерфейса в hbm.xml

<class name="Cat" proxy="ICat"...>...</class>

Пример 4.5. Проксирование интерфейса в аннотациях

@Entity @Proxy(proxyClass=ICat.class) public class Cat implements ICat { ... }

Вы также должны избегать объявления методов как public final, поскольку это снова ограничит возможность создания прокси из этого класса. Если вы хотите использовать класс с методами public final, вы должны явно отключить проксирование. Для этого см. Пример 4.2. «Отключение прокси в hbm.xml» и Пример 4.3. «Отключение прокси в аннотациях».

4.1.4. Объявление аксессоров и мутаторов для постоянных полей (необязательно)

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

Свойства не должны быть объявлены общедоступными (public). Hibernate может сохранять свойство, объявленное с модификаторами доступа package, protected или private.

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

Подкласс должен также соблюдать первое и второе правила. Он наследует своё свойство идентификатора от суперкласса 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(), если вы:

Hibernate гарантирует эквивалентность постоянной (persistent) идентификации (записи базы данных) и идентификатора Java только внутри определенной области сессии (session). Когда вы смешиваете экземпляры, полученные в разных сессиях, вы должны реализовать equals() и hashCode(), если хотите иметь смысловую семантику для Set.

Наиболее очевидным способом является реализация equals()/hashCode() путем сравнения значения идентификатора обоих объектов. Если значение одинаковые, то оба должны быть одной и той же записью в базе данных, поскольку они равны. Если оба добавлены в Set, у вас будет только один элемент в Set). К сожалению, вы не можете использовать этот подход с генерируемыми идентификаторами. Hibernate присваивает значения идентификатора объектам, которые являются постоянными; вновь созданный экземпляр не будет иметь никакого значения идентификатора. Кроме того, если экземпляр несохранён и в настоящее время находится в Set, его сохранение присвоит ему идентификатору. Если equals() и hashCode() основаны на значении идентификатора, хэш-код будет меняться, нарушая контракт Set. См. веб-сайт Hibernate для всесторонего рассмотрения этой проблемы. Это не проблема Hibernate, но обычная семантика Java идентичности и равенства объектов.

Рекомендуется использовать equals() и hashCode(), используя Business key equality (Равенство бизнес-ключа). Равенство бизнес-ключа означает, что метод equals() сравнивает только свойства, которые образуют бизнес-ключ. Это ключ, который идентифицирует наш экземпляр в реальном мире (естественный (natural) ключ кандидата):

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; } }

Бизнес-ключ не должен быть таким же твердым, как кандидат первичного ключа базы данных (см. Параграф 13.1.3 «Рассмотрение идентичности объекта»). Неизменяемые или уникальные свойства обычно являются хорошими кандидатами на бизнес-ключ.

4.4. Динамические модели

Заметка

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

Постоянные сущности необязательно должны быть представлены как классы POJO или как объекты JavaBean во время выполнения. Hibernate также поддерживает динамические модели (используя Map во время выполнения). При таком подходе вы не записываете постоянные классы, а только файлы отражений.

По умолчанию Hibernate работает в обычном режиме POJO. Вы можете установить режим представления сущности по умолчанию для определенного SessionFactory, используя параметр конфигурации default_entity_mode (см. таблицу 3.3 «Свойства конфигурации Hibernate»).

Следующие примеры демонстрируют представление с использованием Map. Во-первых, в файле отражения должно быть объявлено entity-name вместо имени класса или в дополнение к нему:

<hibernate-mapping>
<class entity-name="Customer">
<id name="id" type="long" column="ID"> <generator class="sequence"/> </id>
<property name="name" column="NAME" type="string"/>
<property name="address" column="ADDRESS" type="string"/>
<many-to-one name="organization" column="ORGANIZATION_ID" class="Organization"/>
<bag name="orders" inverse="true" lazy="false" cascade="all"> <key column="CUSTOMER_ID"/> <one-to-many class="Order"/> </bag>
</class> </hibernate-mapping>

Хотя ассоциации объявляются с использованием имен целевого класса, целевой тип ассоциаций также может быть динамической сущностью, а не POJO.

После установки режима сущности по умолчанию в dynamic-map для SessionFactory вы можете, во время исполнения (runtime), работать с картами Map:

Session s = openSession();
Transaction tx = s.beginTransaction();
// Создать клиента Map david = new HashMap(); david.put("name", "David");
// Создать организацию Map foobar = new HashMap(); foobar.put("name", "Foobar Inc.");
// Объдинить их david.put("organization", foobar);
// Сохранить обоих s.save("Customer", david); s.save("Organization", foobar);
tx.commit(); s.close();

Одним из основных преимуществ динамического отображения является быстрое время обработки прототипов, без необходимости реализации класса сущности. Тем не менее, вы теряете проверку типа времени компиляции и, вероятно, будете иметь дело со многими исключениями во время выполнения. В результате отображения Hibernate схема базы данных может быть легко нормализована, что позволяет добавить правильную реализацию модели домена "поверх" позже.

Режимы представления сущностей также могут быть установлены для каждой сессии (Session):

Session dynamicSession = pojoSession.getSession(EntityMode.MAP);
// Создать клиента Map david = new HashMap(); david.put("name", "David"); dynamicSession.save("Customer", david); ... dynamicSession.flush(); dynamicSession.close() ... // Продолжить на pojoSession

Обратите внимание, что вызов метода getSession() с использованием EntityMode выполняется в Session API, а не в SessionFactory. Таким образом, новая сессия (Session) разделяет базовое JDBC соединение, транзакцию и другую контекстную информацию. Это означает, что вам не нужно вызывать функции flush() и close() на вторичной сессии (Session), а также оставлять транзакцию и обработку подключения основной единице (unit) работы.

4.5. Tuplizers (Кортежизаторы)

(tuple - кортеж (мат.))

org.hibernate.tuple.Tuplizer и его под-интерфейсы отвечают за управление конкретным представлением части данных, которое даёт org.hibernate.EntityMode. Если данный фрагмент данных рассматривается как структура данных, то tuplizer — это то, что знает, как создать такую структуру данных, как извлекать значения из такой структуры данных и как вводить значения в такую структуру данных. Например, для режима сущностей POJO соответствующий tuplizer знает, как создать POJO через его конструктор. Он также знает, как получить доступ к свойствам POJO, используя определённые атрибуты свойств.

Существует 2 (высокоуровневых) вида Tuplizer`ов:

Пользователи также могут подключать свои собственные Tuplizer`ы. Возможно, вам потребуется реализация java.util.Map, отличная от java.util.HashMap, которая используется в режиме сущности динамической карты (dynamic-map entity-mode). Или, возможно, вам понадобится определить другую стратегию генерации прокси, чем та, которая используется по умолчанию. Оба будут достигнуты путем определения пользовательской реализации tuplizer. Определения Tuplizer привязаны к отображению сущности или компонента, которому они предназначены для управления. Возвращаясь к примеру нашей сущности Customer, Пример 4.6. Указать пользовательские tuplizer`ы в аннотациях показывает, как указать пользовательский org.hibernate.tuple.entity.EntityTuplizer, используя аннотации, в то время как Пример 4.7. Указать пользовательские tuplizer`ы в hbm.xml показывает, как сделать то же самое в hbm.xml

Пример 4.6. Указать пользовательские tuplizer`ы в аннотациях

@Entity
@Tuplizer(impl = DynamicEntityTuplizer.class)
public interface Cuisine {
    @Id
    @GeneratedValue
    public Long getId();
    public void setId(Long id);
public String getName(); public void setName(String name);
@Tuplizer(impl = DynamicComponentTuplizer.class) public Country getCountry(); public void setCountry(Country country); }

Пример 4.7. Указать пользовательские tuplizer`ы в hbm.xml

<hibernate-mapping>
    <class entity-name="Customer">
        <!--
            Переопределение dynamic-map entity-mode
            tuplizer для сущности customer
        -->
        <tuplizer entity-mode="dynamic-map"
                class="CustomMapTuplizerImpl"/>
<id name="id" type="long" column="ID"> <generator class="sequence"/> </id>
<!-- другие свойства --> ... </class> </hibernate-mapping>

4.6. EntityNameResolvers

org.hibernate.EntityNameResolver — это контракт на разрешение имени сущности данного экземпляра сущности. Интерфейс определяет единственный метод resolveEntityName, который передает экземпляр сущности и, как ожидается, возвращает соответствующее имя сущности (null разрешен и указывает, что распознаватель не знает, как разрешить имя сущности данного экземпляра сущности). Вообще говоря, org.hibernate.EntityNameResolver будет наиболее полезен в случае динамических моделей. Одним из примеров может быть использование проксированных интерфейсов в качестве вашей доменной модели. В тестовом наборе hibernate показан пример в точности этого стиля использования  org.hibernate.test.dynamicentity.tuplizer2. Вот фрагмент кода из этого пакета для иллюстрации.

/**
 * A very trivial JDK Proxy InvocationHandler implementation where we proxy an
 * interface as the domain model and simply store persistent state in an internal
 * Map.  This is an extremely trivial example meant only for illustration.
 */
public final class DataProxyHandler implements InvocationHandler {
	private String entityName;
	private HashMap data = new HashMap();
	public DataProxyHandler(String entityName, Serializable id) {
		this.entityName = entityName;
		data.put( "Id", id );
	}
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		String methodName = method.getName();
		if ( methodName.startsWith( "set" ) ) {
			String propertyName = methodName.substring( 3 );
			data.put( propertyName, args[0] );
		}
		else if ( methodName.startsWith( "get" ) ) {
			String propertyName = methodName.substring( 3 );
			return data.get( propertyName );
		}
		else if ( "toString".equals( methodName ) ) {
			return entityName + "#" + data.get( "Id" );
		}
		else if ( "hashCode".equals( methodName ) ) {
			return new Integer( this.hashCode() );
		}
		return null;
	}
	public String getEntityName() {
		return entityName;
	}
	public HashMap getData() {
		return data;
	}
}
public class ProxyHelper {
    public static String extractEntityName(Object object) {
        // Our custom java.lang.reflect.Proxy instances actually bundle
        // their appropriate entity name, so we simply extract it from there
        // if this represents one of our proxies; otherwise, we return null
        if ( Proxy.isProxyClass( object.getClass() ) ) {
            InvocationHandler handler = Proxy.getInvocationHandler( object );
            if ( DataProxyHandler.class.isAssignableFrom( handler.getClass() ) ) {
                DataProxyHandler myHandler = ( DataProxyHandler ) handler;
                return myHandler.getEntityName();
            }
        }
        return null;
    }
    // разные другие методы ....
}
/**
 * The EntityNameResolver implementation.
 *
 * IMPL NOTE : An EntityNameResolver really defines a strategy for how entity names
 * should be resolved.  Since this particular impl can handle resolution for all of our
 * entities we want to take advantage of the fact that SessionFactoryImpl keeps these
 * in a Set so that we only ever have one instance registered.  Why?  Well, when it
 * comes time to resolve an entity name, Hibernate must iterate over all the registered
 * resolvers.  So keeping that number down helps that process be as speedy as possible.
 * Hence the equals and hashCode implementations as is
 */
public class MyEntityNameResolver implements EntityNameResolver {
    public static final MyEntityNameResolver INSTANCE = new MyEntityNameResolver();
    public String resolveEntityName(Object entity) {
        return ProxyHelper.extractEntityName( entity );
    }
    public boolean equals(Object obj) {
        return getClass().equals( obj.getClass() );
    }
    public int hashCode() {
        return getClass().hashCode();
    }
}
public class MyEntityTuplizer extends PojoEntityTuplizer {
	public MyEntityTuplizer(EntityMetamodel entityMetamodel, PersistentClass mappedEntity) {
		super( entityMetamodel, mappedEntity );
	}
	public EntityNameResolver[] getEntityNameResolvers() {
		return new EntityNameResolver[] { MyEntityNameResolver.INSTANCE };
	}
    public String determineConcreteSubclassEntityName(Object entityInstance, SessionFactoryImplementor factory) {
        String entityName = ProxyHelper.extractEntityName( entityInstance );
        if ( entityName == null ) {
            entityName = super.determineConcreteSubclassEntityName( entityInstance, factory );
        }
        return entityName;
    }
    ...

Чтобы зарегистрировать пользователя org.hibernate.EntityNameResolver, необходимо одно из двух:

  1. Внедрить настраиваемый tuplizer (см. параграф Tuplizers (Кортежизаторы)), реализующий метод getEntityNameResolvers().
  2. Зарегистрировать его с помощью org.hibernate.impl.SessionFactoryImpl (который является классом реализации для org.hibernate.SessionFactory), используя метод registerEntityNameResolver().