Глава 20. Повышение производительности

Оглавление
  1. 20.1. Стратегии выборки
    1. 20.1.1. Работа с ленивыми ассоциациями
    2. 20.1.2. Настройка стратегий выборки
    3. 20.1.3. Одиносторонняя ассоциация прокси
    4. 20.1.4. Инициализация коллекций и прокси
    5. 20.1.5. Использование пакетной выборки
    6. 20.1.6. Использование выборки подзапросов
    7. 20.1.7. Выборка профилей
    8. 20.1.8. Использование ленивой выборки свойств
  2. 20.2. Кэш второго уровня
    1. 20.2.1. Отображения кэшей
    2. 20.2.2. Стратегия: только для чтения
    3. 20.2.3. Стратегия: чтение/запись
    4. 20.2.4. Стратегия: нестрогое чтение/запись
    5. 20.2.5. Стратегия: транзакционная
    6. 20.2.6. Совместимость с поставщиком кэша/параллельной стратегией
  3. 20.3. Управление кэшами
  4. 20.4. Кэш запросов
    1. 20.4.1. Включение кэширования запросов
    2. 20.4.2. Области кэша запросов
  5. 20.5. Улучшение байткода
    1. 20.5.1. Реализация интерфейса org.hibernate.engine.spi.ManagedEntity
    2. 20.5.2. Инструмент времени выполнения (Runtime)
    3. 20.5.3. Инструмент времени построения (Build-time)
  6. 20.6. Понимание производительности коллекций
    1. 20.6.1. Таксономия
    2. 20.6.2. List, Map, idbag и Set — наиболее эффективные коллекции для обновления
    3. 20.6.3. Bag и List являются наиболее эффективными обратными коллекциями
    4. 20.6.4. Удаление одним «выстрелом»
  7. 20.7. Мониторинг производительности
    1. 20.7.1. Мониторинг SessionFactory
    2. 20.7.2. Метрики

20.1. Стратегии выборки

Hibernate использует стратегию выборки для извлечения ассоциированных объектов, если приложение должно перемещаться по ассоциации. Стратегии выборки могут быть объявлены в отображении O/R метаданных или переопределены определенным запросом HQL или Criteria.

Hibernate определяет следующие стратегии выборки:

Hibernate также различает:

Здесь мы имеем два ортогональных понятия: когда выбирается ассоциация и как она выбирается. Важно, чтобы вы не путали их. Мы используем fetch для настройки производительности. Мы можем использовать lazy, чтобы определить контракт, для которого данные всегда доступны в любом отсоединённом экземпляре определенного класса.

20.1.1. Работа с ленивыми ассоциациями

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

Если вы установите hibernate.default_batch_fetch_size, Hibernate будет использовать оптимизацию пакетной выборки для ленивой выборки. Эта оптимизация также может быть включена на более узком уровне.

Имейте в виду, что доступ к ленивой ассоциации вне контекста открытой сессии Hibernate приведет к исключению. Например:

s = sessions.openSession();
Transaction tx = s.beginTransaction();
User u = (User) s.createQuery("from User u where u.name=:userName") .setString("userName", userName).uniqueResult(); Map permissions = u.getPermissions();
tx.commit(); s.close();
Integer accessLevel = (Integer) permissions.get("accounts"); // Ошибка!

Поскольку коллекция разрешений не была инициализирована после закрытия Session, коллекция не сможет загрузить его состояние. Hibernate не поддерживает ленивую инициализацию для отсоединённых объектов . Это можно устранить переместив код, который читает из коллекции, в место перед тем, как транзакция будет зафиксирована (commit).

Кроме того, вы можете использовать «нелинивую» коллекцию или ассоциацию, указав lazy=«false» для отображения ассоциации. Однако предполагается, что ленивая инициализация будет использоваться практически для всех коллекций и ассоциаций. Если вы определяете слишком много «нелинивых» ассоциаций в своей объектной модели, Hibernate будет извлекать всю базу данных в память в каждой транзакции.

С другой стороны, вы можете использовать join fetching (связывающую выборку), которая по своей природе не является ленивой, вместо select fetching (выборки запросом) в конкретной транзакции. Теперь мы объясним, как настроить стратегию выборки. В Hibernate механизмы выбора стратегии выборки идентичны для однозначных ассоциаций и коллекций.

20.1.2. Настройка стратегий выборки

Select fetching (выборка запросом) (по умолчанию) чрезвычайно неэффективна из-за проблемы «N + 1 selects» (количества select, равное N + 1), поэтому мы захотим включить join fetching (связывающую выборку) в документе отображения:

<set name="permissions"
            fetch="join">
    <key column="userId"/>
    <one-to-many class="Permission"/>
</set
<many-to-one name="mother" class="Cat" fetch="join"/>

Стратегия выборки (fetch), определенная в документе отображения, влияет на:

Независимо от используемой стратегии выборки, определённый «неленивый» граф гарантируемо будет загружен в память. Это может, однако, привести к нескольким немедленным select-запросам, которые используются для выполнения конкретного запроса HQL.

Обычно, документ отображения не используется для настройки выборки. Вместо этого мы сохраняем поведение по умолчанию и переопределяем его для конкретной транзакции, используя выборку с left join в HQL. Это говорит Hibernate получить ассоциацию быстро, в первом select-запросе, используя outer join. В API запросов Criteria вы должны использовать setFetchMode(FetchMode.JOIN).

Если вы хотите изменить стратегию выборки, используемую get() или load(), вы можете использовать запрос Criteria. Например:

User user = (User) session.createCriteria(User.class)
                .setFetchMode("permissions", FetchMode.JOIN)
                .add( Restrictions.idEq(userId) )
                .uniqueResult();

Это эквивалент Hibernate того, что некоторые решения ORM называют «планом извлечения (fetch plan)».

Совершенно иной подход к проблемам с «N + 1 selects» — использовать кэш второго уровня.

20.1.3. Одиносторонняя ассоциация прокси

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

При запуске Hibernate генерирует прокси по умолчанию для всех постоянных классов и использует их для включения ленивой выборки ассоциаций «многие-к-одному» и «один-к-одному».

Файл отображения может объявить интерфейс для использования в качестве прокси-интерфейса для этого класса с атрибутом proxy. По умолчанию Hibernate использует подкласс класса. Проксируемый класс должен реализовывать конструктор по умолчанию с областью видимости пакета как минимум. Этот конструктор рекомендуется для всех постоянных классов.

Существуют потенциальные проблемы, которые следует учитывать при распространении этого подхода на полиморфные классы. Например:

<class name="Cat" proxy="Cat">
    ......
    <subclass name="DomesticCat">
        .....
    </subclass>
</class>

Во-первых, экземпляры Cat никогда не будут приводимы к DomesticCat, даже если базовый экземпляр является экземпляром DomesticCat:

Cat cat = (Cat) session.load(Cat.class, id);  // создать экземпляр прокси (не дёргая db)
if ( cat.isDomesticCat() ) {                  // дёрнуть db, чтобы инициализировать прокси-сервер
    DomesticCat dc = (DomesticCat) cat;       // Ошибка!
    ....
}

Во-вторых, возможно разбить прокси ==:

Cat cat = (Cat) session.load(Cat.class, id);            // создать экземпляр прокси Cat
DomesticCat dc = 
        (DomesticCat) session.load(DomesticCat.class, id);  // получить новый прокси DomesticCat!
System.out.println(cat == dc);                            // false

Однако ситуация не такая уж плохая, как кажется. Несмотря на то, что теперь у нас есть две ссылки на разные прокси-объекты, базовый экземпляр по-прежнему будет одним и тем же объектом:

cat.setWeight(11.0);  // дёрнуть db, чтобы инициализировать прокси
System.out.println( dc.getWeight() );  // 11.0

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

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

Все эти проблемы связаны с фундаментальными ограничениями в модели одиночного наследования Java. Чтобы избежать этих проблем, ваши постоянные классы должны реализовывать интерфейс, который объявляет свои бизнес-методы. Вы должны указать эти интерфейсы в файле отображения, где CatImpl реализует интерфейс Cat и DomesticCatImpl реализует интерфейс DomesticCat. Например:

<class name="CatImpl" proxy="Cat">
    ......
    <subclass name="DomesticCatImpl" proxy="DomesticCat">
        .....
    </subclass>
</class>

Затем прокси для экземпляров Cat и DomesticCat могут быть возвращены методами load() или iterate().

Cat cat = (Cat) session.load(CatImpl.class, catid);
Iterator iter = session.createQuery("from CatImpl as cat where cat.name='fritz'").iterate();
Cat fritz = (Cat) iter.next();

Заметка

list() обычно не возвращает прокси.

Отношения также лениво инициализируются. Это значит вы должны объявлять какие-либо свойства типа Cat, а не CatImpl.

Некоторые операций не требуют инициализации прокси:

Hibernate будет обнаруживать постоянные классы, которые переопределяют equals() или hashCode().

Выбрав lazy=«no-proxy» вместо стандартного lazy=«proxy», вы можете избежать проблем, связанных с приведением типов. Тем не менее, инструментарий байт-кода времени сборки потребуется, и все операции приведут к немедленной инициализации прокси.

20.1.4. Инициализация коллекций и прокси

Исключение LazyInitializationException будет выброшено Hibernate, если доступ к неинициализированной коллекции или прокси будет осуществляться извне области видимости Session, то есть когда сущность, владеющая коллекцией или имеющая ссылку на прокси, находится в отсоединённом состоянии.

Иногда перед закрытием Session необходимо инициализировать прокси или коллекцию. Вы можете принудительно инициализировать их вызывом cat.getSex() или cat.getKittens().size(), например. Однако это может ввести в заблуждение читателей кода, и это не удобно для общего кода.

Статические методы Hibernate.initialize() и Hibernate.isInitialized() обеспечивают приложение удобным способом работы с лениво инициализированными коллекциями или прокси. Hibernate.initialize(cat) заставит инициализировать прокси, если его Session всё еще открыт. Hibernate.initialize(cat.getKittens()) имеет аналогичный эффект для коллекции котят.

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

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

Вы можете использовать фильтр коллекции, чтобы получить размер коллекции без ее инициализации:

( (Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()

Метод createFilter() также используется для эффективного извлечения подмножеств коллекции без необходимости инициализации всей коллекции:

s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();

20.1.5. Использование пакетной выборки

Используя пакетную выборку, Hibernate может загружать несколько неинициализированных прокси, если к одину прокси есть обращаение. Пакетная выборка — это оптимизация стратегии ленивой выборки. Существует два способа настройки пакетной выборки: на уровне класса и уровне коллекции.

Пакетную выборку для классов/сущностей легче понять. Рассмотрим следующий пример: во время выполнения у вас есть 25 экземпляров Cat, загруженных в Session, и каждый Cat имеет ссылку на owner — типа Person. Класс Person отображается на прокси, lazy=«true». Если вы теперь перебираете всех Cat и вызываете getOwner() на каждом, Hibernate по умолчанию выполнит 25 иструкций SELECT для извлечения owner. Вы можете настроить это поведение, указав размер пакета batch-size в отображении Person:

<class name="Person" batch-size="10">...</class>

Если batch-size указан, Hibernate будет выполнять запросы по требованию, когда необходимо получить доступ к неинициализированному прокси, как указано выше, но разница заключается в том, что вместо того, чтобы запрашивать конкретную прокси-сущность, к которой обращаются, он будет запрашивать больше сущностей за один раз, поэтому при доступе к owner другого прокси он уже может быть инициализирован этой пакетной выборкой, и будет выполнено только несколько запросов (гораздо меньше 25).

Такое поведение контролируется конфигурацией batch-size и стилем пакетной выборки. Конфигурация стиля пакетной выборки (hibernate.batch_fetch_style) является новым улучшением производительности с 4.2.0. Есть три различные стратегии: LEGACY, PADDED и DYNAMIC.

Вы также можете включить пакетную выборку коллекций. Например, если у каждого Person есть ленивая коллекция Cat, а в Session загружается 10 персон, итерация через всех персон будет генерировать 10 SELECT, по одному для каждого вызова getCats(). Если вы включите пакетную выборку для коллекции cats при отображении Person, Hibernate может предварительно выбрать коллекции:

<class name="Person">
    <set name="cats" batch-size="3">
        ...
    </set>
</class>

Например, с batch-size 3 и использованием legacy пакетного стиля, Hibernate будет загружать 3, 3, 3, 1 коллекции в четыре SELECT. Опять же, значение атрибута зависит от ожидаемого количества неинициализированных коллекций в конкретном Session.

Пакетная выборка коллекций особенно полезна, если у вас есть вложенное дерево элементов, т. е. типичный шаблон «Биль материалов (bill-of-materials)». Однако вложенный набор или материализованный путь может быть лучшим вариантом для деревьев с преобладанием чтения.

20.1.6. Использование выборки подзапросов

Если требуется использовать ленивую коллекцию или однозначный прокси Hibernate загрузит все из них, повторно выполнив исходный запрос в подзапросе (subselect). Это работает так же, как и пакетная выборка, но без поэтапной загрузки.

20.1.7. Выборка профилей

Еще один способ повлиять на стратегию выборки для загрузки связанных объектов — это то, что называется выборка профиля, который является именованной конфигурацией, связанной с org.hibernate.SessionFactory, но включённой по имени в org.hibernate.Session. После включения в org.hibernate.Session, профиль выборки будет влиять на этот org.hibernate.Session, пока он не будет явно отключен.

Так что это значит? Объясним это что на примере, показывающем различные доступные подходы к настройке профиля выборки:

Пример 20.1. Указание профиля выборки с использованием @FetchProfile

@Entity
@FetchProfile(name = "customer-with-orders", fetchOverrides = {
   @FetchProfile.FetchOverride(entity = Customer.class, association = "orders", mode = FetchMode.JOIN)
})
public class Customer {
   @Id
   @GeneratedValue
   private long id;
private String name;
private long customerNumber;
@OneToMany private Set<Order> orders; // стандартные getter/setter ... }

Пример 20.2. Указание профиля выборки с использованием <fetch-profile> вне узла <class>

<hibernate-mapping>
    <class name="Customer">
        ...
        <set name="orders" inverse="true">
            <key column="cust_id"/>
            <one-to-many class="Order"/>
        </set>
    </class>
    <class name="Order">
        ...
    </class>
    <fetch-profile name="customer-with-orders">
        <fetch entity="Customer" association="orders" style="join"/>
    </fetch-profile>
</hibernate-mapping>

Пример 20.3. Указание профиля выборки с использованием <fetch-profile> внутри узла <class>

<hibernate-mapping>
    <class name="Customer">
        ...
        <set name="orders" inverse="true">
            <key column="cust_id"/>
            <one-to-many class="Order"/>
        </set>
        <fetch-profile name="customer-with-orders">
            <fetch association="orders" style="join"/>
        </fetch-profile>
    </class>
    <class name="Order">
        ...
    </class>
</hibernate-mapping>

Теперь, когда вы получаете ссылку на конкретного клиента, набор заказов этого клиента будет ленивым, означающим, что мы еще не загрузили эти заказы из базы данных. Обычно это хорошо. Теперь давайте скажем, что у вас есть такой случай, когда более эффективно загружать клиента и его заказы вместе. Одним из способов является использование стратегий «динамической выборки» с помощью запросов HQL или Criteria . Но другой вариант для достижения этого — использовать профиль выборки. Следующий код загрузит как клиента, так и его заказы:

Пример 20.4. Активация профиля выборки для данного Session

Session session = ...;
session.enableFetchProfile( "customer-with-orders" );  // name matches from mapping
Customer customer = (Customer) session.get( Customer.class, customerId );

Заметка

Определения @FetchProfile являются глобальными, и неважно, на каком классе вы их размещаете. Вы можете поместить аннотацию @FetchProfile либо в класс, либо в пакет (package-info.java). Чтобы определить несколько профилей извлечения для того же класса или пакета, можно использовать @FetchProfiles.

20.1.8. Использование ленивой выборки свойств

Hibernate поддерживает ленивый выборку отдельных свойств. Этот техника оптимизации также известна как выборка групп. Обратите внимание, что это в основном маркетинговый ход; оптимизация чтения строк гораздо важнее оптимизации чтения столбцов. Тем не менее, только загрузка некоторых свойств класса может быть полезна в крайних случаях. Например, когда унаследованные таблицы содержат сотни столбцов, а модель данных не может быть улучшена.

Чтобы включить ленивую загрузку свойств, установите атрибут lazy в конкретных свойствах отображений:

<class name="Document">
       <id name="id">
        <generator class="native"/>
    </id>
    <property name="name" not-null="true" length="50"/>
    <property name="summary" not-null="true" length="200" lazy="true"/>
    <property name="text" not-null="true" length="2000" lazy="true"/>
</class>

Ленивая загрузка свойств требует использования инструментов байт-кода времени сборки. Если ваши постоянные классы не будут расширены, Hibernate будет игнорировать настройки ленивых свойств и вернёться к немедленной выборке.

Для использования инструментов байт-кода используйте следующую задачу Ant:

<target name="instrument" depends="compile">
    <taskdef name="instrument" classname="org.hibernate.tool.instrument.InstrumentTask">
        <classpath path="${jar.path}"/>
        <classpath path="${classes.dir}"/>
        <classpath refxml:id="lib.class.path"/>
    </taskdef>
    <instrument verbose="true">
        <fileset dir="${testclasses.dir}/org/hibernate/auction/model">
            <include name="*.class"/>
        </fileset>
    </instrument>
</target>

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

Вы можете форсировать обычную «нетерпеливую (eager)» выборку свойств, используя fetch all properties в HQL.

20.2. Кэш второго уровня

Hibernate Session представляет собой кэш уровня транзакции постоянных данных. Можно настроить кэш кластера или уровня JVM (SessionFactory-уровня) для каждого класса и коллекции по отдельности. Вы даже можете подключить кластерный кэш. Имейте в виду, что кэши не знают об изменениях, вносимых в постоянное хранилище другим приложением. Тем не менее, они могут быть настроены на регулярное истечение актуальности кэшированных данных.

Таблица 20.1. Поставщики кэша

Кэш Класс поставщика Тип Кластерная безопастность Поддерживается кэш запросов
ConcurrentHashMap (только для целей тестирования, в модуле hibernate-testing) org.hibernate.testing.cache.CachingRegionFactory память да
EHCache org.hibernate.cache.ehcache.EhCacheRegionFactory память, диск, транзакционный, кластерный да да
Infinispan org.hibernate.cache.infinispan.InfinispanRegionFactory транзакционный, кластерный (ip multicast) да (репликация или инвалидация) да (clock sync req.)

20.2.1. Отображения кэшей

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

По умолчанию сущности не входят в кэш второго уровня, и мы рекомендуем придерживаться этого параметра. Однако вы можете переопределить это, установив элемент shared-cache-mode в файле persistence.xml или используя свойство javax.persistence.sharedCache.mode в вашей конфигурации. Возможны следующие значения:

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

Заметка

Рекомендуется определять стратегию параллельного использования кэша для каждой сущности, а не использовать глобальную. Для этого используйте аннотацию @org.hibernate.annotations.Cache.

Пример 20.5. Определение стратегии параллельного использования кэша через @Cache

@Entity 
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Forest { ... }

Hibernate также позволяет вам кэшировать содержимое коллекции или идентификаторы, если коллекция содержит другие сущности. Используйте аннотацию @Cache для свойства коллекции.

Пример 20.6. Кэширование коллекций с помощью аннотаций

@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@JoinColumn(name="CUST_ID")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public SortedSet getTickets() {
    return tickets;
}

Пример 20.7. «Аннотация @Cache с атрибутами» показывает аннотации @org.hibernate.annotations.Cache с их атрибутами. Они позволяют определить стратегию кэширования и область заданного кэша второго уровня.

Пример 20.7. Аннотация @Cache с атрибутами

@Cache(
    CacheConcurrencyStrategy usage();                      (1)
    String region() default "";                            (2)
    String include() default "all";                        (3)
)
  1. usage: заданная стратегия параллельного использования кэша (NONE, READ_ONLY, NONSTRICT_READ_WRITE, READ_WRITE, TRANSACTIONAL)
  2. region (необязательный): область кэша (по умолчанию для fqcn класса или имя роли fq коллекции)
  3. include (необязательный): «all» включает все свойства. «non-lazy» включает только неленивые свойства (по умолчанию «all»).

Давайте теперь посмотрим на файлы отображения Hibernate. Для настройки кэша второго уровня используется элемент <cache> для отображения классов или коллекций. При рассмотрении примера 20.8. «Hibernate элемент отображения <cache>» параллели с анотивами очевидны.

Пример 20.8. Hibernate элемент отображения <cache>

<cache usage="transactional|read-write|nonstrict-read-write|read-only" (1)
       region="RegionName"                                             (2)
       include="all|non-lazy" />                                       (3)
  1. usage (обязательный): указывает стратегию кэширования: transactional, read-write, nonstrict-read-write или read-only
  2. region (необязательный: по умолчанию — имя класса или коллекции): указывает имя области кэша второго уровня
  3. include (необязательный: по умолчанию — all) non-lazy: указывает, что свойства сущности, отображаемой с помощью lazy=«true», не могут быть кэшированы, когда включена ленивая выборка уровня атрибута

В качестве альтернативы <cache> вы можете использовать элементы <class-cache> и <collection-cache> в файле hibernate.cfg.xml.

Давайте теперь рассмотрим различные стратегии использования.

20.2.2. Стратегия: только для чтения

Если ваше приложение должно читать, но не изменять экземпляры постоянного класса, можно использовать кэш только для чтения read-only. Это простейшая и оптимальная стратегия исполнения. Она даже безопасна для использования в кластере.

20.2.3. Стратегия: чтение/запись

Если приложение должно обновлять данные, может потребоваться кэш чтения-записи read-write. Эта стратегия кэширования никогда не должна использоваться, если требуется сериализованный уровень изоляции транзакций. Если кэш используется в среде JTA, вы должны указать свойство hibernate.transaction.manager_lookup_class и называть стратегию для получения JTA TransactionManager. В других средах вы должны убедиться, что транзакция завершена когда вызоваются Session.close() или Session.disconnect(). Если вы хотите использовать эту стратегию в кластере, вы должны убедиться, что реализация кэша поддерживает блокировку. Встроенные поставщики кэша не поддерживают блокировку.

20.2.4. Стратегия: нестрогое чтение/запись

Если приложение только изредка нуждается в обновлении данных (т. е. если крайне маловероятно, что две транзакции будут пытаться одновременно обновлять один и тот же элемент), а строгая изоляция транзакций не требуется, может потребоваться кэш нестрогого чтения-записи nonstrict-read-write. Если кэш используется в среде JTA, вы должны указать hibernate.transaction.manager_lookup_class. В других средах вы должны убедиться, что транзакция завершена когда вызоваются Session.close() или Session.disconnect().

20.2.5. Стратегия: транзакционная

Стратегия транзакционного (transactional) кэширования обеспечивает поддержку полностью транзакционных поставщиков кэширования, таких как JBoss TreeCache. Такой кэш может использоваться только в среде JTA, и вы должны указать hibernate.transaction.manager_lookup_class.

20.2.6. Совместимость с поставщиком кэша/параллельной стратегией

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

Таблица 20.2. Поддержка стратегии параллельного кэша

Кэш read-only nonstrict-read-write read-write transactional
ConcurrentHashMap (не предназначен для использования в производстве) да да да
EHCache да да да да
Infinispan да да

20.3. Управление кэшами

Всякий раз, когда вы передаете объект для операций save(), update() или saveOrUpdate(), и всякий раз, когда вы извлекаете объект с помощью load(), get(), list(), iterate() или scroll(), этот объект добавляется во внутренний кэш Session.

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

Пример 20.9. Исключительное вытеснение экземпляра из кэша первого уровня с использованием Session.evict()

ScrollableResult cats = sess.createQuery("from Cat as cat").scroll(); //a huge result set
while ( cats.next() ) {
    Cat cat = (Cat) cats.get(0);
    doSomethingWithACat(cat);
    sess.evict(cat);
}

Session также предоставляет метод contains() для определения того, принадлежит ли экземпляр кэшу сессии.

Чтобы вытеснить все объекты из кэша сессии, вызовите Session.clear().

Для кэша второго уровня существуют методы, определенные в SessionFactory для вытеснения кэшированного состояния экземпляра, всего класса, экземпляра коллекции или всей коллекции.

Пример 20.10. Вытеснение кэша второго уровня через SessionFactoty.evict() и SessionFacyory.evictCollection()

sessionFactory.evict(Cat.class, catId); //вытеснить определённый Cat
sessionFactory.evict(Cat.class);  //вытеснить все Cats
sessionFactory.evictCollection("Cat.kittens", catId); //вытеснить определённую коллекцию kittens
sessionFactory.evictCollection("Cat.kittens"); //вытеснить все коллекции kitten

CacheMode управляет тем, как определённая сессия взаимодействует с кэшем второго уровня:

Чтобы просмотреть содержимое области второго уровня или кэша запросов, используйте API статистики:

Пример 20.11. Просмотр записей кэша второго уровня через Statistics API

Map cacheEntries = sessionFactory.getStatistics()
        .getSecondLevelCacheStatistics(regionName)
        .getEntries();

Вам нужно будет включить статистику и (необязательно) заставить Hibernate сохранять записи кэша в более читаемом формате:

Пример 20.12. Включение статистики Hibernate

hibernate.generate_statistics true
hibernate.cache.use_structured_entries true

20.4. Кэш запросов

Наборы результатов запроса также можно кэшировать. Это полезно только для запросов, которые часто запускаются с одинаковыми параметрами.

20.4.1. Включение кэширования запросов

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

hibernate.cache.use_query_cache true

Этот параметр создает две новые области кэша:

Важно

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

Как упоминалось выше, большинство запросов не извлекают выгоду из кэширования или их результатов. Таким образом, по умолчанию отдельные запросы не кэшируются даже после включения кэширования запросов. Чтобы включить кэширование результатов для конкретного запроса, вызовите org.hibernate.Query.setCacheable(true). Этот вызов позволяет запросить поиск существующих результатов кэша или добавить его результаты в кэш при его выполнении.

Заметка

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

20.4.2. Области кэша запросов

Если вам требуется микроконтроль политик истечения срока действия кэша запросов, вы можете указать область именованного кэша для конкретного запроса, вызвав Query.setCacheRegion().

List blogs = sess.createQuery("from Blog blog where blog.blogger = :blogger")
        .setEntity("blogger", blogger)
        .setMaxResults(15)
        .setCacheable(true)
        .setCacheRegion("frontpages")
        .list();

Если вы хотите заставить кэш запросов обновить один из его регионов (не обращайте внимания на какие-либо результаты кэширования, которые он находит там), вы можете использовать org.hibernate.Query.setCacheMode(CacheMode.REFRESH). В сочетании с областью, определенной для данного запроса, Hibernate будет выборочно принудительно обновлять результаты, кэшированные в этой конкретной области. Это особенно полезно в тех случаях, когда базовые данные могут быть обновлены через отдельный процесс и являются гораздо более эффективной альтернативой массовому вытеснению региона через org.hibernate.SessionFactory.evictQueries().

20.5. Улучшение байткода

Hibernate внутренне нуждается в вхождении (entry) (org.hibernate.engine.spi.EntityEntry), чтобы сообщить текущее состояние объекта относительно его постоянного состояния, когда объект связан с Session. Тем не менее, поддержание этой ассоциации было довольно тяжелой операцией из-за большого количества других правил, которые должны применяться, поскольку с 4.2.0 существует новое усовершенствование, предназначенное для этой цели, что уменьшит связанные с сессией память и перегрузки процессора.

В основном, идея состоит в том, что вместо того, чтобы иметь настроенную (вид тяжелой и обычно обозначаемый как «горячая точка») карту, чтобы посмотреть вверх, мы меняем ее на

EntityEntry entry = (ManagedEntity)entity.$$_hibernate_getEntityEntry();

Существует три способа получить преимущества от этого нового улучшения:

20.5.1. Реализация интерфейса org.hibernate.engine.spi.ManagedEntity

Сущность может сама реализовать этот интерфейс, тогда ответственность за поддержание би-ассоциации, по существу, обеспечивает доступ к информации об ассоциации экземпляра с Session/EntityManager. Подробнее о org.hibernate.engine.spi.ManagedEntity можно узнать из javadoc.

20.5.2. Инструмент времени выполнения (Runtime)

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

Очевидно, что это самый простой способ выбора, поскольку он не требует изменения исходного кода проекта, но он также требует больше памяти и использования CPU по сравнению с первым.

20.5.3. Инструмент времени построения (Build-time)

Помимо вышеупомянутых двух подходов, Hibernate также предоставляет третий вариант, который улучшает время построения байт-кода. Приложения могут использовать расширенные классы сущностей, аннотированные с помощью javax.persistence.Entity или составного javax.persistence.Embeddable.

Ant Task

Чтобы использовать задачу org.hibernate.tool.enhance.EnhancementTask, определите taskdef и вызовите задачу, как показано ниже. Этот код использует предопределённый classpathref и свойство, ссылающееся на каталог скомпилированных классов.

<taskdef name="enhance" classname="org.hibernate.tool.enhance.EnhancementTask" classpathref="enhancement.classpath" />
<enhance>
    <fileset dir="${ejb-classes}/org/hibernate/auction/model" includes="**/*.class"/>
</enhance>

Заметка

EnhancementTask предназначен для полной замены InstrumentTask. Кроме того, он также несовместим с InstrumentTask, поэтому любые существующие инструментальные классы необходимо будет снова собрать из источника.

Maven плагин

Плагин Maven использует дескриптор Mojo для присоединения Mojo к фазе компиляции вашего проекта.

<dependencies>
   <dependency>
      <groupId>org.hibernate.javax.persistence</groupId>
      <artifactId>hibernate-jpa-[SPEC-VERSION]-api</artifactId>
      <version>[IMPL-VERSION]</version>
      <scope>compile</scope>
   </dependency>
</dependencies>
<plugins>
<plugin>
  <groupId>org.hibernate.orm.tooling</groupId>
  <artifactId>hibernate-enhance-maven-plugin</artifactId>
  <version>VERSION</version>
  <executions>
    <execution>
      <goals>
        <goal>enhance</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Gradle плагин

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

apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'hibernate'
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.hibernate:hibernate-gradle-plugin:VERSION'
    }
}
dependencies {
   compile group: 'org.hibernate.javax.persistence', name: 'hibernate-jpa-[SPEC-VERSION]-api', version: '[IMPL-VERSION]'
}

20.6. Понимание производительности коллекций

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

20.6.1. Таксономия

Hibernate определяет три основных типа коллекций:

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

Все индексированные коллекции (карты, списки и массивы) имеют первичный ключ, состоящий из столбцов <key> и <index>. В этом случае обновления коллекции чрезвычайно эффективны. Первичный ключ может быть эффективно проиндексирован, и определённая строка может быть эффективно размещена, когда Hibernate пытается обновить или удалить её.

У наборов есть первичный ключ, состоящий из столбцов <key> и element. Это может быть менее эффективным для некоторых типов элементов коллекции, особенно составных элементов или больших текстовых или двоичных полей, поскольку база данных может не иметь возможности индексировать сложный первичный ключ эффективно. Однако для ассоциаций «один-ко-многим» или «многие-ко-многим», особенно в случае синтетических идентификаторов, он, вероятно, будет столь же эффективным. Если вы хотите, чтобы SchemaExport фактически создал первичный ключ <set>, вы должны объявить все столбцы как not-null=«true».

<idbag> отображения определяют суррогатный ключ, поэтому они эффективны для обновления. По факту, это лучший случай.

Сумки (bags) являются наихудшим случаем, так как они допускают повторяющиеся значения элементов и, поскольку у них нет столбца индекса, первичный ключ не может быть определён. Hibernate не имеет возможности различать повторяющиеся строки. Hibernate разрешает эту проблему, полностью удаляя в одном DELETE и воссоздавая коллекцию всякий раз, когда она изменяется. Это может быть неэффективным.

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

20.6.2. List, Map, idbag и Set — наиболее эффективные коллекции для обновления

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

Существует, возможно, ещё одно преимущество, что индексированные коллекции имеют больше наборов для ассоциаций «много-ко-многим» или наборов значений. Из-за структуры Set, Hibernate не делает UPDATE записи, когда элемент «изменен». Изменения в Set всегда работают через INSERT и DELETE отдельных записей. И снова это соображение не относится к ассоциациям «один-ко-многим».

Наблюдая, что массивы не могут быть ленивыми, вы можете сделать вывод, что списки (lists), карты (maps) и idbags являются наиболее эффективными (не обратными) типами коллекций, с наборами (sets) далеко не так. Вы можете ожидать, что наборы будут наиболее распространённым видом коллекции в приложениях Hibernate. Это связано с тем, что семантика «set» наиболее естественна в реляционной модели.

Однако в хорошо спроектированных моделях домена Hibernate большинство коллекций на самом деле являются ассоциациями «один-ко-многим» с inverse=«true». Для этих ассоциаций обновление обрабатывается «многими-к-одному» концом ассоциации, поэтому соображения эффективности обновления коллекции просто не применяются.

20.6.3. Bag и List являются наиболее эффективными обратными коллекциями

Однако существует конкретный случай, когда сумки (bags), а также списки (lists), намного более эффективны, чем наборы (sets). Для коллекции с inverse=«true», стандартна двунаправленная идиома отношения «один-ко-многим», например, мы можем добавлять элементы в сумку (bag) или список (list) без необходимости инициализации (извлечения) элементов сумки (bag). Это связано с тем, что в отличие от набора (set) Collection.add() или Collection.addAll() всегда должны возвращать true для сумки (bag) или списка (List). Следующий общий код может сделать это намного быстрее:

Parent p = (Parent) sess.load(Parent.class, id);
Child c = new Child();
c.setParent(p);
p.getChildren().add(c);  //no need to fetch the collection!
sess.flush();

20.6.4. Удаление одним «выстрелом»

Иногда удаление элементов коллекции может быть крайне неэффективным. Hibernate не знает, что делать в случае новой пустой коллекции (например, вы вызывали list.clear()). В этом случае Hibernate сделает один DELETE.

Предположим, вы добавили один элемент в коллекцию размером 20, а затем удаляете два элемента. Hibernate сделает одну инструкцию INSERT и две инструкции DELETE, если коллекция не является сумкой (bag). Это, безусловно, желательно.

Однако предположим, что мы удалим восемнадцать элементов, оставив два, а затем добавим новые элементы. Существует два возможных способа:

Hibernate не может знать, что второй вариант, вероятно, быстрее. Было бы нежелательно, чтобы Hibernate был настолько интуитивным, что такое поведение могло бы запутать триггеры базы данных и т. д.

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

One-shot-delete (удаление одним «выстрелом») не применяется к коллекциям, отображённым с inverse=«true».

20.7. Мониторинг производительности

Оптимизация не очень удобна без мониторинга и доступа к показателям производительности. Hibernate предоставляет полный спектр данных о своих внутренних операциях. Статистика в Hibernate доступна на каждому SessionFactory.

20.7.1. Мониторинг SessionFactory

Вы можете получить доступ к метрикам SessionFactory двумя способами. Первый вариант — вызвать sessionFactory.getStatistics() и прочитать или отобразить Statistics самостоятельно.

Hibernate также может использовать JMX для публикации показателей, если вы включите MBean StatisticsService. Вы можете включить один MBean для всего SessionFactory или один на фабрику. См. следующий код минимальных примеров конфигурации:

// Регистрация сервиса MBean для определённого SessionFactory
Hashtable tb = new Hashtable();
tb.put("type", "statistics");
tb.put("sessionFactory", "myFinancialApp");
ObjectName on = new ObjectName("hibernate", tb); // имя объекта MBean
StatisticsService stats = new StatisticsService(); // реализация MBean
stats.setSessionFactory(sessionFactory); // Привязать статистики к SessionFactory
server.registerMBean(stats, on); // регистрация Mbean на сервере
// Регистрация сервиса MBean для всех SessionFactory
Hashtable tb = new Hashtable();
tb.put("type", "statistics");
tb.put("sessionFactory", "all");
ObjectName on = new ObjectName("hibernate", tb); // имя объекта MBean
StatisticsService stats = new StatisticsService(); // реализация MBean
server.registerMBean(stats, on); // регистрация Mbean на сервере

Вы можете активировать и деактивировать мониторинг для SessionFactory:

Статистику можно сбросить программно с помощью метода clear(). Сводка может быть отправлена логгеру (уровень info ) с использованием метода logSummary().

20.7.2. Метрики

Hibernate предоставляет ряд показателей, от базовой информации до более специализированной информации, которая имеет значение только в определённых сценариях. Все доступные счетчики описаны в API интерфейса Statistics в трех категориях:

Например, вы можете проверить запросы к кэшу, отсутствие запрашиваемого в кэше, коэффициент кэширования сущностей, коллекций и запросов, и среднее время запроса. Имейте в виду, что количество миллисекунд может быть аппроксимировано в Java. Hibernate привязан к точности JVM, и на некоторых платформах это может быть точным только до 10 секунд.

Простые геттеры используются для доступа к глобальным метрикам (т. е. не привязаны к конкретной сущности, коллекции, области кэша и т. д.). Вы можете получить доступ к метрикам определённой сущности, коллекции или области кэша через её имя и через ей представление HQL или SQL запросов. Для получения дополнительной информации см. API Javadoc Statistics, EntityStatistics, CollectionStatistics, SecondLevelCacheStatistics и QueryStatistics. Следующий код — простой пример:

Statistics stats = HibernateUtil.sessionFactory.getStatistics();
double queryCacheHitCount = stats.getQueryCacheHitCount(); double queryCacheMissCount = stats.getQueryCacheMissCount(); double queryCacheHitRatio = queryCacheHitCount / (queryCacheHitCount + queryCacheMissCount);
log.info("Query Hit ratio:" + queryCacheHitRatio);
EntityStatistics entityStats = stats.getEntityStatistics( Cat.class.getName() ); long changes = entityStats.getInsertCount() + entityStats.getUpdateCount() + entityStats.getDeleteCount(); log.info(Cat.class.getName() + " changed " + changes + "times" );

Вы можете работать со всеми сущностями, коллекциями, запросами и областями кэшами, получая список имён сущностей, коллекций, запросов и областей кэшей, используя следующие методы: getQueries(), getEntityNames(), getCollectionRoleNames() и getSecondLevelCacheRegionNames().