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