Spring: Data Access
При определении источника данных DataSource в качестве бина можно указать необходимость генерации имени для встроенных, embedded БД (например, H2, Derby, HSQLDB). Это удобно для тестирования:
1 |
|
Для embedded БД необходимо удалять все тестовые БД, которые были созданы. Для каждой БД могут быть свои запросы, но принцип одинаков:
1 |
|
JdbcTemplate
В случае если результат запроса содержит строки с несколькими колонками, следует использовать реализацию RowMapper
. Но если результат содержит 1 колонку примитивного типа, ее можно получить сразу:
1 | Long count = jdbcTemplate.queryForObject("select count(*) from users", Long.class); |
Можно воспользоваться оберточными методами queryForInt
, *Long
, *Byte
и т.д. Так же существует метод queryForMap
, который преобразует строку из результата запроса к БД в Map<String, ?>
. Метод queryForList
конвертирует результат в List<?>
. В случае если в результате запроса много колонок, можно сделать так:
1 | List<Map<String, Object>> list = jdbcTemplate.queryForList("select * from users"); |
Результатом будет список Map
, где ключом будет название колонки в ответе от БД, а значением - значение из БД для данной колонки.
JdbcTemplate и RowCallbackHandler
В JdbcTemplate
есть метод query
, который может принимать RowCallbackHandler
. Этот интерфейс реализует прочтение строки из ResultSet
:
1 | class MyHandler implements RowCallbackHandler { |
Вызов метода processRow
выполняется для каждой строки.
ResultSetExtractor
Отличается от Handler
тем что должен вернуть domain объект в результате обработки строки.
1 | class MyObjectExtractor implements ResultSetExtractor<MyObject> { |
NamedParameterJdbcTemplate
В классическом JdbcTemplate передаются параметры через “?”, в NamedParameterJdbcTemplate
- через именованные параметры:
1 | JdbcTemplate jdbcTemplate = ... |
Ошибки при работе
DataAccessException
. Базовое исключение для всех остальных. Наследуется отRuntimeException
;NoTransientDataAccessException
. Базовое исключение для ветки ошибок, которые не могут получить запрашиваемые данные (например, по причине их отсутствия в ответе);RecoverableDataAccessException
. Исключение выкидывается когда операция с ошибкой, предположительно, может быть успешной при повторной попытке (например, после переподключения);TransientDataAccessException
. Возникает при параллельном доступе к данным или при сетевых проблемах;ScriptException
. Возникает если при инициализации БД, при выполнии скрипта возникла ошибка (т.е., больше актуально для тестирования).
TransactionTemplate
Утилитарный класс, созданный для возможности работы с транзакциями напрямую (минуя аннотацию @Transactional
). Интсрумент удобен для точечного применения. Ниже приведен пример “ручного” управление транзакциями с помощью бина TransactionTemplate
:
1 |
|
Работа в транзакции
Существует 4 основных реализации:
- Jdbc Spring. Средствами Spring. Для включения транзакции необходимо аннотирование метода с помощью
@Transactional
. Начало и конец транзакции будет соответствовать началу и окончанию метода; - Hibernate. Требует определения бина
HibernateTransactionManager
; - JPA. Требует бин
JpaTransactionManager
; - Enterprise JTA. Требует работы на сервере приложений (например, WebLogic) с настроенным источником данных через JNDI. Поскольку работает в Java EE, все транзакции являются распределенными - т.е. рассматриваются как единая транзакция всеми сервера в EE кластере. Существуют и реалиации не требующие EE приложений, а выступающие как подключаемые библиотеки, например, Atomikos.
Провейдеры транзакций
DataSourceTransactionManager
. Базовая реализация локальной транзакции для JDBC и MyBatis;HibernateTranscationManager
. Реализация при использовании Hibernate;JpaTransactionManager
. Реализации при использовании JPA;JtaTransactionManager
. Реализация, используемая Java EE, а так же библиотеками распределенных транзакций, например, Atomikos;WebLogicJtaTransactionManager
. Реализация JTA, только для сервера WebLogic.
Конфигурации транзакций
Самый простой способ добавить транзакции в Spring приложение:
- Определить бин менеджера
1 |
|
Реализацию менеджера можно выбрать любую, в примерах ниже будет использоваться стандартная для Spring.
- Активировать транзакции в любой
@Configuration
:
1 |
|
- Аннотировать метод, который следует заключить в транзакцию
@Transactional
:
1 |
|
Для того чтобы менеджер транзакций гарантированно запустился (с реализацией по ум. Spring), можно:
1 |
|
Данная конфигурация будет при использовании нескольких источников данных и для каких-то нужна одна реализация менеджеров транзакций, для других - другая. В стандартной реализации Spring не сможет определить менеджера по умолчанию и выдаст ошибку. Конфигуратор вышел установит менеджер транзакций по умолчанию принудительно - когда Spring не сможет найти альтернативу, будет использован именно он. Причем, ситуация описанная выше будет видна только при попытке открыть транзакцию (в Spring реализована @Lazy
инъекция). Второй способ принудительно устанавливать менеджера транзакций по умолчанию - добавление @Primary
аннотации.
@Transactional
В терминологии Spring - Atomic units of work. Используется только вместе с public модификаторами, т.к. иначе прокси объекты их просто не увидят.
Менеджер транзакций можно установить принудительно для каждой аннотации:
1 |
|
В терминологии Spring, методы с данной аннотацией носят название Declarative transactions.
Принцип работы @Transactional
Когда создается бин, Spring проверяет все аннотации, которые есть у него и те, которые есть у его public методов. Если Spring замечает какой-то элемент, который требует транзакции, то он создает прокси обертку, которая и контролирует работу с транзакцией (так Spring работает не только с @Transactional
, но и с другими подобными аннотацими - это говорит о том что чрезмерное количество аннотаций приводит к большому количеству скрытых прокси объектов, которыми сложно управлять).
Если в рамках оргинального метода будет вызван какой-либо метод этого же бина, работа все равно будет продолжаться в рамках действующего прокси объекта, т.е. в рамках действующей транзакции. Даже если у вызываемого метода будет своя аннотация @Transactional
с отличной propagation
.
Аналогично работа осуществляется и для другого бина, у которого вызывается метод @Transactional
. Вызов произойдет уже не у его метода, а метода прокси объекта, в котором будет специфическая обработка транзакции. Реализация транзакции в рамках прокси объекта будет решать как поступить с транзакцией, руководствуясь значением параметра propagation
.
Опции для @Transactional
transactionManager
. применяемый менеджер (точнее бин), который будет контролировать выполнение транзакции;readOnly
. Помечает что транзакция только для чтения. Spring попытается добавить оптимизацию запросу;propagation
. Определяет выделение транзакции. Бывает следующих видов:REQUIRED
. Метод требует любую транзакцию. Будет использовать текущую, если таковой нет, то она будет создана;REQUIRES_NEW
. Требует новую транзакцию, текущая перейдет в статус ожидания;NESTED
. Похожа наREQUIRED
, но в случае отката транзакции (rollback) действия, совершенные до(!) nested транзакции останутся ожидать подтверждения (или отката собственной транзакции). В терминологии баз данных это Save Point Rollback. Если ошибки или отката не произошло, nested транзакция будет выполнена по окончании метода; родительская транзакция так и останется в статусе ожидания (если она была);MANDATORY
. Если транзакция существует, то она будет использоваться; если действующей транзакции нет, то будет ошибка;NEVER
. Выполнение в рамках хотябы какой-нибудь транзакции вызовет ошибку;NOT_SUPPORTED
. Если транзакция существует, она будет приостановлена и восстановится по завершении метода. Все что будет выполнено в рамках метода не будет применено в транзакции (иными словами, все действия по изменениям не войдут в транзакцию, а значит, не будут применены);SUPPORTS
. Если транзакция существует, то она будет использована. Если действующей транзакции нет - действия не будут выполнены в рамках транзакции.
isolation
. Уровень изоляции. Бывает:DEFAULT
. Принято по умолчанию и зависит от параметров БД;READ_UNCOMMITED
. Изменения текущей транзакции доступны другим, даже если они без commit’а, только на чтение;READ_COMMITED
. Чтение доступно только после commit’аREPEATABLE_READ
. В рамках одной транзакции данные всегда будут возвращены теми, которыми они были при первом прочтении. Т.е. если транзакция_В внесла изменения и сделала commit, а транзакция_А ранее уже читала измененные данные, то обновление данные транзакция_В не увидит - прочитаются только старыеSERIALIZABLE
. Максимальный уровень изоляции. Данный уровень может провоцировать ошибки параллельного изменения записей, что следует обрабатывать соответствующим образом. Часто используется в сочетании с блокировкой записи на уровне БД (наложение блокировки зависит от типа используемой БД)
timeout
. Количество миллисекунд, по истечении которых следует посчитать транзакцию выполненной с ошибкой. Важно отметить что таймаут считается только до(!) получения транзакцией управления, время исполнения метода в расчете не учитывается. По умолчанию времяtimeout
определяется менеджером транзакции и может быть изменено для всего менеджера;rollbackFor
. Тип ошибок, при которых транзакцию следуте откатить. По ум. откатываются толькоRuntimeException
! Указывать следует только checked исключения;noRollbackFor
. Обратная кrollbackFor
. Можно указатьRuntimeException
(или дочерние к ней) - тогда транзакция не будет откатываться никогда.
Параметры rollbackFor
и noRollbackFor
можно совмещать.
Некоторые особенности @Transaction
:
- Аннотацию можно накладывать на класс и тогда ко всем public методам будут применены данные конфигурации транзакции, но если у методов будут свои
@Transaction
, они будут иметь приоритет; - Использовать транзакции следует аккуратно и по месту.
Тестирование транзакций
При тестировании специфическим методам может потребоваться дополнительная SQL подготовка. В этом помогают аннотации из группы SQL (@Sql
и @SqlConfig
):
1 |
@Commit
Аннотация накладывается на метод если нужно подтвердить результаты выполнения теста. По умолчанию тестовые методы всегда делают откат транзакций (а следовательно, и изменений) по их окончании.
@Rollback
Аннотация откатывает транзакцию, если установлена со значение true (по умолчанию). Использование совместно с @Commit
запрещено (да и не логично).
@BeforeTransaction
Накладывается на метод, который должен быть вызван до начала транзакции соответствующим менеджером транзакций.
1 |
|
Метод выполняется в уже инициализированной транзакции, но до(!) тестового метода. Метод с @BeforeTransaction
выполняется в Spring контексте.
Распределенные транзакции
Это транзакции, охватывающие несколько транзакционных ресурсов (например, несколько разных БД, или БД + JMS и т.д.). Требует JTA и специальные XA драйвера. JTA провайдеры: Wildfly, JOTM, Atomikos, Narayana, Bitronix и др.
Hibernate и ORM
Для работы потребуется:
SessionFactory
. Является ключевым элементом, thread-safe, shareable, immutable. Как правило, на приложение достаточно одного экземпляра;Session
. Основной компонент, который используется для связи между Java и Hibernate. Получить объект можно так:sessionFactory.getCurrentSession()
;- HibernateTransactionManager (Hibernate версий 5 и 6). Является реализацией
TransactionManager
для Spring транзакцией. Требует создания бина:
1 |
|
LocalSessionFactoryBuilder
. Может создаватьSessionFactory
. Для этого потребуется dataSource, пакет(ы) с доменными сущностями и Hibernate настройками:
1 |
|
Hibernate параметры
hibernate.dialect
. Диалект для общения Hibernate с БД. Зависит от СУБД;hibernate.hbm2ddl.auto
. Определяет что необходимо сделать при старте приложения. Допускается:none
- не делает ничего (по умолчанию);create-only
- попытается создать БД;drop
- удаляет все элементы из БД, которые описаны в сущностях Hibernate;create
- удаление и повторное создание схемы;create-drop
- аналогичноcreate
, но по завершении выполнения программы (или завершения тестов) БД будет очищена;validate
- Hiberante проверяет соответствует ли состояние БД той схеме, которая описана в Hibnernate сущностях;update
- требование к Hibernate обновить схему в БД до состояния схожего с описанным в Hibernate сущностях;
hibernate.show_sql
. Выводит SQL в системный поток записи. Логгеры могут перенаправлять системный поток в соответствии с реализацией, тем самым вывод запросов может быть определен, например, в файл;hibernate.format_sql
. При включенномhibernate.show_sql
запросы, которые будут писаться в системный поток, будет в отформатированом виде;hibernate.user_sql_comment
. Hibernate попытается вставить свои комментарии с его видением что происходит при запросе.
Запрос через Hibernate
Hibernate предоставляет набор методов, которые позволяют работать сессии с объектами. Работа осуществляется посредством бина типа Session:
update(T)
. Записывает измнения в БД;persist(T)
. Добавляет сущность в БД и сохраняет изменения. Если в определении множественности (аннотации@OneToOne
,@OneToMany
,@ManyToOne
,@ManyToMany
) указан параметрcascade="persist"
, то сохраняться будут и они;save(T)
. Сохраняет новую сущность в БД и возвращает его сгенерированный идентификатор. Если в определении множественности указаноcascade="save-update"
, то соответствующий методsave
будет применен и к нимsaveOrUpdate(T)
. При наличии объекта в БД будет выполненupdate
; если объекта в БД нет, будет выполненsave
.
Hibernate и @Repository
Утилитарный элемент, который упрощает работу с
1 |
|
HQL vs Native
Выполнение HQL возможно при вызове createQuery
, стандартный SQL запрос выполняется при помощи createNativeQuery
.
Обработка ошибок
По умолчанию Hibernate выдает собственные ошибки (наследуются от HibernateException
, а оно в свою очередь, от RuntimeException
), но можно указать необходимость выдачи Spring ошибок. Для этого достаточно назначить менеджером транзакций HibernateTransactionManager
. Вторым вариантом является определение Exception Transalator
:
1 |
|
Connection pool
Пул соединений, который будет использоваться Hibernate, а точнее будет создан дополнительный источника данных - DataSource - модифицированной под работу в пуле (наиболее известная реализация - HikariCP).
1 |
|
JPA
Ключевыми компонентами JPA являются:
- Persistence Context. Контекст, описывающий сущности;
- Entity Manager. Объект (менеджер), управляющий сущностями. Выполняет
create
,update
,select
,delete
. Должен реализоватьjavax.persistence.EntityManager
. Как правило, их жизненый цикл ограничен транзакцией; - EntityManagerFactory. Отвечает за создание
EntityManagerPersitenceUnit
. Юниты, которые определяют связь между данными в БД и Java классами. Эту связь передаютEntityManager
для последующей инициализации; - JPA Provider. Фреймворк, реализующий стандарт JPA для Spring. Например, Hibernate, EclipseLink, ApacheOpenJPA, DataNucleus;
Подключение JPA
Алгоритм будет продемонстрирован на примере Hibernate:
- Создать
EntityManagerFactory
с указанием поставщика JPA:
1 |
|
- Создать менеджера транзакций JPA:
1 |
|
- Создать репозиторий:
1 |
|
Методы EntityManager
find
. Поиск по первичному ключу;createQuery
. Создание JPQL запроса. Принимает строковый JPQL;createNamedQuery
. Принимает имя хранимого строкового JPQL запроса и выполняет его;persist
. Добавляет сущность в контекст (аналогSQL INSERT
);merge
. Сливает сущность с текущим контекстом (аналогSQL UPDATE
);flush
. Применяет все изменения, выполненные в рамках транзакции;refresh
. Обновляет сущность из(!) DataSource. Т.е. изменения, сделанные ранее в сущности будет отменены;remove
. Удаляет объект из контекста.
Запрос с помощью JPA
JPA своими запросами очень похож на HQL:
1 | User user = (User) entityManager.createQuery("select u from User u where u.username=? and u.password=?").setParameter(1, "anonymous").setParameter(2, "pass").getSingleResult(); |
Допускается использование именованных параметров:
1 | entityManager.createQuery("select u from User u where u.username=:name and u.password=:pswd").setParameter("name", "anonymous").setParameter("pswd", "pass").getSingleResult(); |
@NamedQuery
Могут объединяться в списки (для удобства) и объявляться в любом бине (как правило, объявляется либо в сущности, либо в связанном с ним репозитории):
1 |
|
CriteriaBuilder
Запрос можно строить и через criteriaBuilder
, который имеит синтаксис, основанный на Java. Предназначен чтобы строить динамичные запросы (с использование Hibernate) с большим количеством условий, хотя и сложен в анализе и не всегда итоговый SQL строится оптимально.
Spring Data JPA
Ключевым интерфейсом является Repository<T, ID extendsSerializable>
. Дочерним к нему CrudRepository
, который добавляет create-update-read-delete методы. Затем расширяется PagingAndSortRepository
, который включает методы для сортировки и пагинации. Конечным и наиболее используемым интерфейсом из семейства Data JPA является JpaRepository
, который незначительно расширяет все предыдущие.
Все они имею аннотацию @NoRepositoryBean
, которая указывает Spring на то что не следует создавать объект репозитория исключительно для указанных репозиториев. Если потребуется отключить создание объектов для своих интерфейсов, можно сделать так же.
@RepositoryDefinition
Репозиторий JPA можно создавать через наследование. Альтернативным вариантом является аннотация:
1 |
|
Равнозначно:
1 |
|
Запросы в JPA репозиториях
Репозитории можно дополнять запросами:
1 | public interface UserRepo extends JpaRepository<User, Long> { |
Как это работает
Для каждого репозитория JPA, Spring создает прокси объект, в котором всем методам (унаследованы от Data JPA интерфейсов) создает стандартную реализацию.
Объединенные Repository
Идея состоит в том чтобы можно было объединить несколько сторонних реализаций в один репозиторий, дополненный каким-то кодом, который создал разработчик.
1 | extends |
, где UserCustomRepo
- это интерфейс; UserCustomRepoImpl
- это класс, реализующий некие дополнительные методы; UserRepo
- JPA репозиторий, который создает стандартные JPA методы, а так же включает в себя реализации из UserCustomRepoImpl
только тех методов, которые описаны в UserCustomRepo
(т.е. методы, которые перегружены с помощью @Override
в классе UserCustomRepoImpl
, реализующем интерфейс UserCustomRepo
).
При этом UserRepo
может наследовать логику не только одного Custom Repository, но из нескольких.
Включение JPA репозиториев
Для включения следует добавить аннотацию в @Configuration
:
1 |
|