Spring: Data Access

Spring: Data Access


При определении источника данных DataSource в качестве бина можно указать необходимость генерации имени для встроенных, embedded БД (например, H2, Derby, HSQLDB). Это удобно для тестирования:

1
2
3
4
5
6
@Bean
public DataSource dataSource() {
EmbeddedDataBuilder builder = new EmbeddedDatabaseBuilder();
EmbeddedDatabase db = builder.setType(H2).addScripts().generateUniqueName().build();
return db;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@Configuration
public class MyDbConfig {
@Bean(destroyMethod="destroy")
public DbCleanup cleanup(JdbcTemplate jdbcTemplate) {
return new DbCleanup(jdbcTemplate);
}
}

class DbCleanup {
JdbcTemplate jdbcTemplate;

public DbCleanup(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public void destroy() {
jdbcTemplate.execute("DROP ALL OBJECTS DELETE FILES;");
}
}

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
2
3
class MyHandler implements RowCallbackHandler {
public void processRow(ResultSet rs) throws SQLException { ... }
}

Вызов метода processRow выполняется для каждой строки.

ResultSetExtractor

Отличается от Handler тем что должен вернуть domain объект в результате обработки строки.

1
2
3
class MyObjectExtractor implements ResultSetExtractor<MyObject> {
public MyObject extractData(ResultSet rs) throws SQLException, DataAccessException {...}
}

NamedParameterJdbcTemplate

В классическом JdbcTemplate передаются параметры через “?”, в NamedParameterJdbcTemplate - через именованные параметры:

1
2
3
4
5
JdbcTemplate jdbcTemplate = ...
jdbcTemplate.queryForObject("select * from users where name=? and password=?", rowMapper, userName, userPass);

NamedParameterJdbcTemplate jdbcTemplate = ...
jdbcTemplate.queryForObject("select * from users where name=:username and password=:userpass", Map.of("username", name, "userpass", pass), rowMapper);

Ошибки при работе

  • DataAccessException. Базовое исключение для всех остальных. Наследуется от RuntimeException;
  • NoTransientDataAccessException. Базовое исключение для ветки ошибок, которые не могут получить запрашиваемые данные (например, по причине их отсутствия в ответе);
  • RecoverableDataAccessException. Исключение выкидывается когда операция с ошибкой, предположительно, может быть успешной при повторной попытке (например, после переподключения);
  • TransientDataAccessException. Возникает при параллельном доступе к данным или при сетевых проблемах;
  • ScriptException. Возникает если при инициализации БД, при выполнии скрипта возникла ошибка (т.е., больше актуально для тестирования).

TransactionTemplate

Утилитарный класс, созданный для возможности работы с транзакциями напрямую (минуя аннотацию @Transactional). Интсрумент удобен для точечного применения. Ниже приведен пример “ручного” управление транзакциями с помощью бина TransactionTemplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class MyService {
private final MyRepo repo;
private final TransactionalTemplate tx;

public MyService(MyRepo repo, final TransactionalTemplate tx) {
this.repo = repo;
this.tx = tx;
}

public MyObject findAny() {
return tx.execute(txStatus -> {
try {
return repo.find(...);
} catch(Exception e) {
txStatus.setRollbackOnly();
return null;
}
});
}
}

Работа в транзакции

Существует 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. Определить бин менеджера
1
2
3
4
5
6
7
8

@Configuration
public class MyConfig {
@Bean
public TransactionManager txManager(DataSource datasource) {
return new DataSourceTransactionManager(datasource);
}
}

Реализацию менеджера можно выбрать любую, в примерах ниже будет использоваться стандартная для Spring.

  1. Активировать транзакции в любой @Configuration:
1
2
3
@Configuration
@EnableTransactionManager
public class MyConfig {...}
  1. Аннотировать метод, который следует заключить в транзакцию @Transactional:
1
2
@Transactional
public MyObject find() { ... }

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

1
2
3
4
5
6
7
8
@Configuration
@EnableTransactionManager
public class MyConfig implements TransactionManagementConfigurer {
@Override
public TransactionManager annotationDrivenTransactionManager() {
return txManager();
}
}

Данная конфигурация будет при использовании нескольких источников данных и для каких-то нужна одна реализация менеджеров транзакций, для других - другая. В стандартной реализации Spring не сможет определить менеджера по умолчанию и выдаст ошибку. Конфигуратор вышел установит менеджер транзакций по умолчанию принудительно - когда Spring не сможет найти альтернативу, будет использован именно он. Причем, ситуация описанная выше будет видна только при попытке открыть транзакцию (в Spring реализована @Lazy инъекция). Второй способ принудительно устанавливать менеджера транзакций по умолчанию - добавление @Primary аннотации.

@Transactional

В терминологии Spring - Atomic units of work. Используется только вместе с public модификаторами, т.к. иначе прокси объекты их просто не увидят.

Менеджер транзакций можно установить принудительно для каждой аннотации:

1
2
@Transactional(transactionManager="mySecondTxMgr")
public List<User> findAll() { ... }

В терминологии 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
2
3
4
5
@Sql(scripts="classpath:...")
@Sql(scripts="classpath:...", config=@SqlConfig(transactionMode=TransactionMode.ISOLATED),
executionPhase=ExecutionPhase.AFTER_TEST_METHOD
@Test
public void listAllInIsolatedTransaction() {...}

@Commit

Аннотация накладывается на метод если нужно подтвердить результаты выполнения теста. По умолчанию тестовые методы всегда делают откат транзакций (а следовательно, и изменений) по их окончании.

@Rollback

Аннотация откатывает транзакцию, если установлена со значение true (по умолчанию). Использование совместно с @Commit запрещено (да и не логично).

@BeforeTransaction

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

1
2
3
4
5
@SpringJUnitConfig(...)
public class MyTests {
@BeforeTransaction
public void checkBeforeTx() { ... }
}

Метод выполняется в уже инициализированной транзакции, но до(!) тестового метода. Метод с @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
2
3
4
@Bean
public TransactionManager txManager() {
return new HibernateTransactionManager(sessionFactory());
}
  • LocalSessionFactoryBuilder. Может создавать SessionFactory. Для этого потребуется dataSource, пакет(ы) с доменными сущностями и Hibernate настройками:
1
2
3
4
@Bean 
public SessionFactory sessionFactory() {
return new LocalSessionFactoryBuilder(dateSource()).scanPackages(“my.company.entries”).addProperties(hibernateProperties()).buildSessionFactory();
}

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
2
3
4
5
6
7
8
9
@Repository
public class MyRepo {
@Autowired
sessionFactory sessionFactory;

public List<User> findAll() {
return sessionFactory.getCurrentSession().createQuery(“FROM User”).list();
}
}

HQL vs Native

Выполнение HQL возможно при вызове createQuery, стандартный SQL запрос выполняется при помощи createNativeQuery.

Обработка ошибок

По умолчанию Hibernate выдает собственные ошибки (наследуются от HibernateException, а оно в свою очередь, от RuntimeException), но можно указать необходимость выдачи Spring ошибок. Для этого достаточно назначить менеджером транзакций HibernateTransactionManager. Вторым вариантом является определение Exception Transalator:

1
2
3
4
5
6
7
8
9
@Bean
public PersistenceExceptionTranslationPostProcessor exPostProcessor() {
return new PersistenceExceptionTranslationPostProcessor ();
}

@Bean
public HibernateExceptionTranslator hiberExTranslator() {
return new HibernateExceptionTranslator();
}

Connection pool

Пул соединений, который будет использоваться Hibernate, а точнее будет создан дополнительный источника данных - DataSource - модифицированной под работу в пуле (наиболее известная реализация - HikariCP).

1
2
3
4
5
6
7
8
9
10
@Bean
public DataSource dateSource() {
try{
HikariConfig hc = new HikariConfig();
hc.setDriverClassName(...).setJdbcUrl(...).setUsername(...).setPassword().setMaximumPoolSize().setConnectionTestQuery().setPoolName();
return new HikariDataSource(hc);
} catch(Exception e) {
throw new RuntimeException(e);
}
}

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:

  1. Создать EntityManagerFactory с указанием поставщика JPA:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableTransactionManager
public class MyConf {
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setPackagesToScan(“my.comp.entity”);
factory.setDataSource(dataSource());
factory.setJpaVendorAdapter(new HiberanteJpaVendorAdapter());
factory.setJpaProperties(properties());
factory.afterPropertiesSet();
return factory.getNativeEntityManagerFactory();
}
}
  1. Создать менеджера транзакций JPA:
1
2
3
4
@Bean
public TransactionManager transactionManager() {
return new JpaTransactionManager(entityManager());
}
  1. Создать репозиторий:
1
2
3
4
5
6
7
8
9
@Repository
public class JPAMyObjectRepo {
private EntityManager entityManager;

@PersistenceContext
void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
}

Методы 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
2
3
4
5
6
7
8
9
10
@Entity
@NamedQueries({@NamedQuery(name="req1", query="select ..."), @NamedQuery(name="second_req", query="select ...")})
public class User {...}

@Repository
public class MyUserRepo() {
public List<User> findAll() {
return entityManager.craeteNamedQuery("second_req").setParameter(1, "").list();
}
}

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
2
@RepositoryDefinition(domainClass=User.class, idClass=Long.class)
public interface UserRepo {...}

Равнозначно:

1
2
@Repository
public interface UserRepo extends JpaRepository<User, Long> {...}

Запросы в JPA репозиториях

Репозитории можно дополнять запросами:

1
2
3
4
5
6
7
public interface UserRepo extends JpaRepository<User, Long> {
@Query("select u from User where u.username like ?1")
Optional<User> findByUsername(String username);

@Query("select u from User where u.username=:name and u.password=:pswd")
Optional<User> findByUsername(@Param("name") String username, @Param("pswd") String pass);
}

Как это работает

Для каждого репозитория JPA, Spring создает прокси объект, в котором всем методам (унаследованы от Data JPA интерфейсов) создает стандартную реализацию.

Объединенные Repository

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
                                              extends                                           
:::::::::::::::::::::::::::::::::::::::::::::: :::::::::::::::::::::::::::::::::::::::::::::
: .. .. ..
: UserCustomRepo .. <<-- .. UserRepo ..
: .. .. ..
:............................................. ...........................................::
^
^
|
|
| implements
|
|
.......................|.......................
: :
: UserCustomRepoImpl :
: :
...............................................

, где UserCustomRepo - это интерфейс; UserCustomRepoImpl - это класс, реализующий некие дополнительные методы; UserRepo - JPA репозиторий, который создает стандартные JPA методы, а так же включает в себя реализации из UserCustomRepoImpl только тех методов, которые описаны в UserCustomRepo (т.е. методы, которые перегружены с помощью @Override в классе UserCustomRepoImpl, реализующем интерфейс UserCustomRepo).

При этом UserRepo может наследовать логику не только одного Custom Repository, но из нескольких.

Включение JPA репозиториев

Для включения следует добавить аннотацию в @Configuration:

1
2
3
4
@Configuration
@EnableJpaRepositories(basePackages={"...", "..."})
@EnableTransactionManager
public class JdbcConfig {...}
 Comments
Comment plugin failed to load
Loading comment plugin