Введение
В этой статье я собираюсь показать вам лучший способ использования аннотации Spring Transactional.
Это один из лучших методов, которые я применял при разработке RevoGain, веб-приложения, которое позволяет вам рассчитать прибыль, полученную при торговле акциями, товарами или криптовалютами с помощью Revolut.
Аннотация Spring Transactional
Начиная с версии 1.0, Spring предлагал поддержку управления транзакциями на основе AOP, что позволяло разработчикам декларативно определять границы транзакций. Я знаю об этом, потому что читал руководство осенью 2004 года:
Очень скоро после этого, в версии 1.2, Spring добавил поддержку аннотации @Transactional, что еще больше упростило настройку границ транзакций бизнес-единиц работы.
Аннотация @Transactional
содержит следующие атрибуты:
value
иtransactionManager
— эти атрибуты могут быть использованы для предоставления ссылки наTransactionManager
, которая будет использоваться при обработке транзакции для аннотированного блокаpropagation
— определяет, как границы транзакции распространяются на другие методы, которые будут вызваны прямо или косвенно из аннотированного блока. По умолчаниюpropagation
задается какREQUIRED
и значит, что она запускается, если еще нет ни одной транзакции. В противном случае текущая транзакция будет использована выполняющимся на данный момент методом.timeout
иtimeoutString
— определяют максимальное количество секунд, в течение которых текущему методу разрешено работать, прежде чем будет выброшено исключениеTransactionTimedOutException
readOnly
— определяет, является ли текущая транзакция доступной только для чтения или для записи.rollbackFor
иrollbackForClassName
— определяют один или несколько классовThrowable
, для которых текущая транзакция будет откатываться. По умолчанию транзакция откатывается, если возникаетRuntimException
илиError
, но не откатывается, если возникает проверенноеException
.noRollbackFor
иnoRollbackForClassName
— определяют один или несколько классовThrowable
, для которых текущая транзакция не будет откатываться. Обычно вы используете эти атрибуты для одного или нескольких классовRuntimException
, для которых вы не хотите откатывать данную транзакцию.
К какому уровню относится аннотация Spring Transactional?
Аннотация @Transactional
принадлежит к сервисному уровню (Service), потому что именно он отвечает за определение границ транзакций.
Не используйте ее на веб-уровне, поскольку это может увеличить время отклика транзакции базы данных и усложнить предоставление правильного сообщения об ошибке для определенной ситуации (например, согласованность, дедлок, получение блокировки, оптимистическая блокировка).
Для уровня DAO (Data Access Object) или репозитория требуется транзакции на уровне приложения, но она должна распространяться с сервисного уровня.
Лучший способ использования аннотации Spring Transactional
На сервисном уровне вы можете иметь как связанные, так и не связанные с базой данных службы. Если в конкретном сценарии использования необходимо их сочетать, например, когда нужно сделать парсинг заданного оператора, создать отчет и занести результаты в базу данных, то лучше всего, если транзакция с ней (БД) будет осуществлена как можно позже.
По этой причине можно использовать нетранзакционную шлюзовую службу, например, RevolutStatementService
:
@Service
public class RevolutStatementService {
@Transactional(propagation = Propagation.NEVER)
public TradeGainReport processRevolutStocksStatement(
MultipartFile inputFile,
ReportGenerationSettings reportGenerationSettings) {
return processRevolutStatement(
inputFile,
reportGenerationSettings,
stocksStatementParser
);
}
private TradeGainReport processRevolutStatement(
MultipartFile inputFile,
ReportGenerationSettings reportGenerationSettings,
StatementParser statementParser
) {
ReportType reportType = reportGenerationSettings.getReportType();
String statementFileName = inputFile.getOriginalFilename();
long statementFileSize = inputFile.getSize();
StatementOperationModel statementModel = statementParser.parse(
inputFile,
reportGenerationSettings.getFxCurrency()
);
int statementChecksum = statementModel.getStatementChecksum();
TradeGainReport report = generateReport(statementModel);
if(!operationService.addStatementReportOperation(
statementFileName,
statementFileSize,
statementChecksum,
reportType.toOperationType()
)) {
triggerInsufficientCreditsFailure(report);
}
return report;
}
}
Метод processRevolutStocksStatement
нетранзакционный, и поэтому можно использовать стратегию Propagation.NEVER
для обеспечения того, чтобы этот метод никогда не вызывался из активной транзакции.
Поэтому statementParser.parse
и метод generateReport
выполняются в нетранзакционном контексте, поскольку мы не хотим устанавливать соединение с базой данных и поддерживать его, когда нам нужно выполнить только обработку на уровне приложения.
Только operationService.addStatementReportOperation
должна выполняться в транзакционном контексте, и по этой причине addStatementReportOperation
использует аннотацию @Transactional
:
@Service
@Transactional(readOnly = true)
public class OperationService {
@Transactional(isolation = Isolation.SERIALIZABLE)
public boolean addStatementReportOperation(
String statementFileName,
long statementFileSize,
int statementChecksum,
OperationType reportType) {
...
}
}
Обратите внимание, что addStatementReportOperation
переопределяет уровень изоляции по умолчанию и указывает, что этот метод выполняется в транзакции базы данных SERIALIZABLE
.
Стоит также отметить, что класс аннотирован @Transactional(readOnly = true)
, это значит, что по умолчанию все методы сервиса будут использовать эту настройку и выполняться в транзакции только для чтения, если только метод не переопределит транзакционные настройки с помощью своего собственного определения @ТгаnѕастіоnаІ
.
Для транзакционных сервисов хорошей практикой является установка атрибута readOnly
в значение true
на уровне класса и переопределение его на основе каждого метода для методов служб, которым требуется запись в базу данных.
Например, UserService
использует тот же шаблон:
@Service
@Transactional(readOnly = true)
public class UserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
...
}
@Transactional
public void createUser(User user) {
...
}
}
В loadUserByUsername
используется транзакция только для чтения, и поскольку мы используем Hibernate, Spring также выполняет некоторые оптимизации в режиме только для чтения.
С другой стороны, createUser
должен записывать информацию в базу данных. Поэтому он переопределяет значение атрибута readOnly
значением по умолчанию, заданным аннотацией @Transactional
, которое соответствует readOnly=false
, что делает транзакцию доступной для чтения и записи.
Еще одним большим преимуществом разделения методов "чтение-запись" и "только для чтения" является то, что мы можем направлять их на разные узлы базы данных, как объясняется в этой статье.
Таким образом, мы можем масштабировать трафик только для чтения, увеличивая количество узлов-реплик. Потрясающе, правда?
Заключение
Аннотация Spring Transactional очень удобна, когда речь идет об определении границ транзакций бизнес-методов.
Хотя значения атрибутов по умолчанию были выбраны правильно, хорошей практикой является предоставление настроек как на уровне класса так и на уровне метода, чтобы разделить варианты использования на нетранзакционные, транзакционные, только для чтения и чтения-записи.
Данный материал подготовили для будущих студентов нового потока курса «Разработчик на Spring Framework», а всех желающих приглашаем на бесплатный открытый урок на тему «Правильный DAO на Spring JDBC». На этом занятии рассмотрим, как использовать всю мощь нативного SQL и при этом написать безопасное, поддерживаемое и тестируемое DAO с использованием Spring JDBC.