Начнем с освещения Hibernate 5 в окружении Spring, используя его для демонстрации подхода Spring к интеграции OR-отображателей. В этом разделе подробно рассмотрены многие вопросы и показаны различные варианты реализации DAO и разграничения транзакций. Большинство из этих шаблонов можно непосредственно переложить на все другие поддерживаемые ORM-инструменты. В последующих разделах этой главы описаны другие ORM-технологии и приведены краткие примеры.

Начиная с версии Spring Framework 5.3, Spring требует наличия Hibernate ORM 5.2+ для HibernateJpaVendorAdapter из Spring, а также для нативной настройки SessionFactory из Hibernate. Настоятельно рекомендуется использовать Hibernate ORM 5.4 для приложений, работа над которыми была только начата. Для использования с HibernateJpaVendorAdapter, Hibernate Search нужно обновить до версии 5.11.6.

Настройка SessionFactory в контейнере Spring

Чтобы не привязывать объекты приложения к жестко закодированному поиску ресурсов, можно определить ресурсы (такие как DataSource из JDBC или SessionFactory из Hibernate) как бины в контейнере Spring. Объекты приложения, которым необходим доступ к ресурсам, получат ссылки на такие предопределенные экземпляры через ссылки на бины, как это показано в определении DAO в следующем разделе.

В следующей выдержке из определения контекста приложения на XML показанов, как установить DataSource из JDBC и SessionFactory из Hibernate поверх него:

<beans>
    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>
    <bean id="mySessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
        <property name="dataSource" ref="myDataSource"/>
        <property name="mappingResources">
            <list>
                <value>product.hbm.xml</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <value>
                hibernate.dialect=org.hibernate.dialect.HSQLDialect
            </value>
        </property>
    </bean>
</beans>

Переход от локального BasicDataSource из Jakarta Commons DBCP к DataSource, расположенному в JNDI (обычно управляемому сервером приложений), является лишь вопросом конфигурации, что и показано в следующем примере:

<beans>
    <jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>

Также можно получить доступ к SessionFactory, расположенной в JNDI, используя JndiObjectFactoryBean / <jee:jndi-lookup> из Spring для её получения и открытия. Однако это, как правило, нетипичное решение вне контекста EJB.

Spring также предоставляет вариант LocalSessionFactoryBuilder, легко взаимодействующий с конфигурацией на основе аннотации @Bean и программной установкой (без участия FactoryBean).

И LocalSessionFactoryBean, и LocalSessionFactoryBuilder поддерживают фоновую начальную загрузку, при этом инициализация Hibernate выполняется параллельно с потоком загрузки приложения для данного исполнителя загрузки (например, SimpleAsyncTaskExecutor). Для LocalSessionFactoryBean это доступно через свойство bootstrapExecutor. В программном LocalSessionFactoryBuilder есть перегруженный метод buildSessionFactory, который принимает аргумент исполнителя начальной загрузки.

Начиная со Spring Framework 5.1, такая нативная настройка Hibernate может также открывать EntityManagerFactory из JPA для типичного JPA-взаимодействия наряду с нативным доступом Hibernate. Подробнее см. в разделе "Нативная настройка Hibernate для JPA".

Реализация DAO на основе простого API-интерфейса Hibernate

В Hibernate есть функция, называемая контекстными сессиями, в которой Hibernate сам управляет одной текущей Session для каждой транзакции. Это примерно эквивалентно синхронизации одной Session из Hibernate для каждой транзакции в Spring. Соответствующая реализация DAO похожа на следующий пример, основанный на обычном API-интерфейсе Hibernate:

Java
public class ProductDaoImpl implements ProductDao {
    private SessionFactory sessionFactory;
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }
    public Collection loadProductsByCategory(String category) {
        return this.sessionFactory.getCurrentSession()
                .createQuery("from test.Product product where product.category=?")
                .setParameter(0, category)
                .list();
    }
}
Kotlin
class ProductDaoImpl(private val sessionFactory: SessionFactory) : ProductDao {
    fun loadProductsByCategory(category: String): Collection<*> {
        return sessionFactory.currentSession
                .createQuery("from test.Product product where product.category=?")
                .setParameter(0, category)
                .list()
    }
}

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

Предыдущий пример DAO следует шаблону внедрения зависимостей. Он прекрасно вписывается в IoC-контейнер Spring, как это было бы, если бы он был написан на основе шаблона HibernateTemplate из Spring. Также можно настроить такой DAO в обычном Java (например, в модульных тестах). Для этого создайте его экземпляр и вызовите setSessionFactory(..) с нужной ссылкой на фабрику. Как определение бина Spring, DAO-объект будет выглядеть следующим образом:

<beans>
    <bean id="myProductDao" class="product.ProductDaoImpl">
        <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>
</beans>

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

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

К счастью, LocalSessionFactoryBean из Spring поддерживает метод SessionFactory.getCurrentSession() из Hibernate для любой транзакционной стратегии Spring, возвращая текущую транзакционную Session, управляемую Spring, даже при использовании HibernateTransactionManager. Стандартной логикой работы этого метода остается возврат текущей Session, связанной с текущей JTA-транзакцией, если таковая имеется. Эта логика работы применяется независимо от того, используете ли вы JtaTransactionManager из Spring, транзакции, управляемые контейнером (CMT-транзакции) из EJB или же интерфейс JTA.

В целом, можно реализовать DAO-объекты на основе обычного API-интерфейса Hibernate, но при этом иметь возможность задействовать их в транзакциях, управляемых Spring.

Разграничение декларативных транзакций

Рекомендуем использовать декларативную поддержку транзакций Spring, которая позволит заменить явные вызовы API-интерфейса для разграничения транзакций в Java-коде на перехватчик транзакций АОП. Можно настроить этот перехватчик транзакций в контейнере Spring с помощью аннотаций Java или XML. Эта возможность использовать декларативные транзакции позволяет избавить бизнес-службы от повторяющегося кода разграничения транзакций и сфокусировать его на добавлении бизнес-логики, которая имеет действительно вещественное значение для приложения.

Прежде чем продолжить, мы настоятельно рекомендуем прочитать раздел "Декларативное управление транзакциями", если вы еще этого не сделали.

Можете пометить уровень служб аннотациями @Transactional и дать контейнеру Spring команду искать эти аннотации и обеспечивать транзакционную семантику для данных аннотированных методов. В следующем примере показано, как это сделать:

Java
public class ProductServiceImpl implements ProductService {
    private ProductDao productDao;
    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }
    @Transactional
    public void increasePriceOfAllProductsInCategory(final String category) {
        List productsToChange = this.productDao.loadProductsByCategory(category);
        // ...
    }
    @Transactional(readOnly = true)
    public List<Product> findAllProducts() {
        return this.productDao.findAllProducts();
    }
}
Kotlin
class ProductServiceImpl(private val productDao: ProductDao) : ProductService {
    @Transactional
    fun increasePriceOfAllProductsInCategory(category: String) {
        val productsToChange = productDao.loadProductsByCategory(category)
        // ...
    }
    @Transactional(readOnly = true)
    fun findAllProducts() = productDao.findAllProducts()
}

В контейнере необходимо настроить реализацию PlatformTransactionManager (в виде бина) и запись <tx:annotation-driven/>, разрешив обработку аннотаций @Transactional во время выполнения. В следующем примере показано, как это сделать:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- SessionFactory, DataSource и т.д. опущены -->
    <bean id="transactionManager"
            class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>
    <tx:annotation-driven/>
    <bean id="myProductService" class="product.SimpleProductService">
        <property name="productDao" ref="myProductDao"/>
    </bean>
</beans>

Разграничение программных транзакций

Можно разграничить транзакции на более высоком уровне приложения, поверх служб доступа к данным более низкого уровня, которые охватывают любое количество операций. Также не существует ограничений на реализацию окружающей бизнес-службы. Ей нужен только PlatformTransactionManager из Spring. Опять же, последнее можно получить откуда угодно, но предпочтительнее будет в виде ссылки на бин через метод setTransactionManager(..). Кроме того, productDAO должен быть установлен методом setProductDao(..). В следующей паре фрагментов показаны диспетчер транзакций и определение бизнес-службы в контексте приложения Spring и пример реализации бизнес-метода:

<beans>
    <bean id="myTxManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>
    <bean id="myProductService" class="product.ProductServiceImpl">
        <property name="transactionManager" ref="myTxManager"/>
        <property name="productDao" ref="myProductDao"/>
    </bean>
</beans>
Java
public class ProductServiceImpl implements ProductService {
    private TransactionTemplate transactionTemplate;
    private ProductDao productDao;
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }
    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }
    public void increasePriceOfAllProductsInCategory(final String category) {
        this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            public void doInTransactionWithoutResult(TransactionStatus status) {
                List productsToChange = this.productDao.loadProductsByCategory(category);
                // поднимаем цены...
            }
        });
    }
}
Kotlin
class ProductServiceImpl(transactionManager: PlatformTransactionManager,
                        private val productDao: ProductDao) : ProductService {
    private val transactionTemplate = TransactionTemplate(transactionManager)
    fun increasePriceOfAllProductsInCategory(category: String) {
        transactionTemplate.execute {
            val productsToChange = productDao.loadProductsByCategory(category)
            // поднимаем цены...
        }
    }
}

TransactionInterceptor из Spring позволяет генерировать любое проверяемое исключение приложения вместе с кодом обратного вызова, в то время как TransactionTemplate ограничен непроверяемыми исключениями внутри обратного вызова. TransactionTemplate запускает откат в случае непроверяемого исключения приложения или если транзакция помечена приложением как подлежащая откату (путем установки TransactionStatus). По умолчанию TransactionInterceptor ведет себя так же, но позволяет конфигурировать политику отката для каждого метода.

Стратегии управления транзакциями

И TransactionTemplate, и TransactionInterceptor делегируют фактическую обработку транзакций экземпляру PlatformTransactionManager (который может быть HibernateTransactionManager (для одной SessionFactory из Hibernate), используя ThreadLocal Session что называется "за кадром") или JtaTransactionManager (делегируя подсистеме JTA контейнера) для приложений Hibernate. Вы даже можете использовать собственную реализацию PlatformTransactionManager. Переход от нативного управления транзакциями Hibernate к JTA (например, при возникновении требований к распределенным транзакциям для определенных развертываний вашего приложения) – это всего лишь вопрос конфигурации. Можно заменить диспетчер транзакций Hibernate на реализацию транзакций JTA из Spring. Как код разграничения транзакций, так и код доступа к данным работают без изменений, поскольку они используют типизированные API-интерфейсы управления транзакциями.

В случае распределенных между несколькими фабриками сессий Hibernate транзакций можно объединить JtaTransactionManager в роли транзакционной стратегии с несколькими определениями LocalSessionFactoryBean. Затем каждый DAO получит одну конкретную ссылку на SessionFactory, переданную в соответствующее свойство бина. Если все базовые источники данных JDBC являются транзакционными и контейнерными, бизнес-служба сможет разграничивать транзакции между любым количеством DAO и любым количеством фабрик сессий без особого проблем, если JtaTransactionManager используется в качестве стратегии.

HibernateTransactionManager и JtaTransactionManager позволяют корректно обрабатывать кэш на уровне JVM с помощью Hibernate без поиска диспетчера транзакций в контейнере или коннектора JCA (если вы не используете EJB для инициирования транзакций).

HibernateTransactionManager может экспортировать Connection из Hibernate на основе JDBC в обычный код доступа JDBC для определенного DataSource. Эта возможность позволяет осуществлять высокоуровневое разграничение транзакций со смешанным доступом к данным Hibernate и JDBC полностью без JTA, при условии, что вы обращаетесь только к одной базе данных. HibernateTransactionManager автоматически открывает транзакцию Hibernate как транзакцию JDBC, если вы настроили переданную SessionFactory с DataSource через свойство dataSource класса LocalSessionFactoryBean. Как вариант, можно явным образом задать DataSource, для которого должны быть открыты транзакции, через свойство dataSource класса HibernateTransactionManager.

Сравнение ресурсов, управляемых контейнерами, и локально определенных ресурсов

Можно переключаться между управляемой контейнером SessionFactory из JNDI и локально определенной, не изменив ни единой строки кода приложения. Вопрос о том, хранить ли определения ресурсов в контейнере или локально в приложении, в основном зависит от используемой транзакционной стратегии. По сравнению с локальной SessionFactory, определяемой Spring, регистрация SessionFactory из JNDI вручную не дает никаких преимуществ. Развертывание SessionFactory через JCA-коннектор Hibernate обеспечивает дополнительную пользу от присутствия в инфраструктуре управления сервером Java EE, но не имеет фактической ценности, сверх этого.

Поддержка транзакций в Spring не привязана к контейнеру. При конфигурировании с использованием любой стратегией, кроме JTA, поддержка транзакций также работает в автономном или тестовом окружении. Поддержка локальных транзакций Spring с одним ресурсом является легкой и полнофункциональной альтернативой JTA, особенно в типичном случае транзакций с одной базой данных. Если для управления транзакциями используются локальные не сохраняющие состояния сессионные бины EJB, будет существовать зависимость и от контейнера EJB, и от JTA, даже если обращаться только к одной базе данных и использовать только те сессионые бины, которые на сохраняют состояние, для предоставления декларативных транзакций через управляемые контейнером транзакции. Прямое использование JTA программно также требует наличия окружения Java EE. JTA включает в себя не только контейнерные зависимости в плане самого JTA и экземпляров DataSource из JNDI. Для транзакций Hibernate, не относящихся к Spring и управляемых JTA, необходимо использовать JCA-коннектор из Hibernate или дополнительный транзакционный код из Hibernate с TransactionManagerLookup, сконфигурированным для надлежащего кэширования на уровне JVM.

Транзакции, управляемые Spring, могут работать с локально определенной SessionFactory из Hibernate так же хорошо, как и с локальным DataSource из JDBC, при условии, что они обращаются к одной базе данных. Таким образом, транзакционную стратегию JTA из Spring необходимо использовать только тогда, когда имеются требования к распределенным транзакциям. JCA-коннектор требует специфических для контейнеров шагов развертывания и (очевидно) средств поддержки JCA в первую очередь. Над такой конфигурацией придется дольше работать, чем при развертывании простого веб-приложения с локальными определениями ресурсов и транзакциями, управляемыми Spring. Кроме того, зачастую требуется Enterprise Edition контейнера, если используется, например, WebLogic Express, который не предоставляет JCA. Приложение Spring с локальными ресурсами и транзакциями, охватывающими одну единственную базу данных, работает в любом веб-контейнере Java EE (без JTA, JCA или EJB), таком как Tomcat, Resin или даже простой Jetty. Кроме того, можно легко повторно использовать такое промежуточное звено в настольных приложениях или тестовых комплектах.

В целом, если вы не используете EJB-бины, придерживайтесь локальной настройки SessionFactory и HibernateTransactionManager или JtaTransactionManager из Spring. Вам будут доступны все преимущества, включая надлежащее транзакционное кэширование на уровне JVM и распределенные транзакции, без всяких неудобств, связанных с развертыванием контейнеров. JNDI-регистрация SessionFactory из Hibernate через JCA-коннектор приносит пользу только при использовании в сочетании с EJB-бинами.

Ложные предупреждения сервера приложений при использовании Hibernate

В некоторых окружениях JTA с очень строгой реализацией XADataSource (в настоящее время это некоторые версии WebLogic Server и WebSphere), если Hibernate сконфигурирован без учета диспетчера транзакций JTA для этого окружения, в журнале сервера приложений могут появляться ложные предупреждения или исключения. Эти предупреждения или исключения указывают на то, что соединение, к которому осуществляется доступ, больше недействительно или доступ к JDBC больше недействителен, возможно, потому что транзакция больше не активна. В качестве примера приведем реальное исключение из WebLogic:

java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
further JDBC access is allowed within this transaction.

Другой распространенной проблемой является утечка соединений после транзакций JTA, если сессии Hibernate (и, возможно, базовые соединения JDBC) не закрыты должным образом.

Эти проблемы можно решить, сделав Hibernate совместимым с диспетчером транзакций JTA, с которым он синхронизируется (вместе со Spring). Существует два варианта, как это сделать:

  • Передайте свой бин JtaTransactionManager из Spring в конфигурацию Hibernate. Самый простой способ – это предоставить ссылку на бин в свойстве jtaTransactionManager для вашего бина LocalSessionFactoryBean (см. раздел "Настройка транзакций Hibernate"). Затем Spring сделает соответствующие стратегии JTA доступными для Hibernate.

  • Также можно явным образом сконфигурировать свойства Hibernate, связанные с JTA, в частности "hibernate.transaction.coordinator_class", "hibernate.connection.handling_mode" и, возможно, "hibernate.transaction.jta.platform" в "hibernateProperties", для LocalSessionFactoryBean (подробности об этих свойствах см. в руководстве по Hibernate).

В оставшейся части данного раздела описана последовательность событий, происходящих с учетом и без учета того, что Hibernate совместим с PlatformTransactionManager из JTA.

Если Hibernate был сконфигурирован без какого-либо учета диспетчера транзакций JTA, при фиксации транзакции JTA происходят следующие события:

  • Транзакция JTA фиксируется.

  • JtaTransactionManager из Spring синхронизируется с транзакцией JTA, поэтому он вызывается обратно через обратный вызов afterCompletion диспетчером транзакции JTA.

  • Среди прочих действий, эта синхронизация служит причиной обратного вызова со стороны Spring в отношении Hibernate через обратный вызов afterTransactionCompletion из Hibernate (используется для очистки кэша Hibernate), после чего последует явный вызов close() для сессии Hibernate, что приведет к тому, что Hibernate попытается close() JDBC-соединение.

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

Если Hibernate сконфигурирован с учетом диспетчера транзакций JTA, при фиксации транзакции JTA происходят следующие события:

  • Транзакция JTA готова к фиксации.

  • JtaTransactionManager из Spring синхронизируется с транзакцией JTA, поэтому транзакция вызывается обратно путем обратного вызова beforeCompletion диспетчером транзакций JTA.

  • Spring учитывает, что Hibernate самостоятельно синхронизируется с транзакцией JTA и начинает вести себя иначе, чем в предыдущем сценарии. В частности, он приводится в соответствие с транзакционным управлением ресурсами из Hibernate.

  • Транзакция JTA фиксируется.

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