Почнемо з висвітлення Hibernate 5 в оточенні Spring, використовуючи його для демонстрації підходу Spring до інтеграції OR-відображувачів. У цьому розділі детально розглянуто багато питань та показано різні варіанти реалізації DAO та розмежування транзакцій. Більшість із цих шаблонів можна безпосередньо перекласти на всі інші інструменти, що підтримуються ORM. У наступних розділах цього розділу описані інші ORM-технології та наведені короткі приклади. 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);
                // raise prices...
            }
        });
    }
}
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 і може належним чином очистити свій кеш.