[Перевод] Лучший способ использовать аннотацию Spring Transactional

image-loader.svg

Введение

В этой статье я собираюсь показать вам лучший способ использования аннотации 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, что делает транзакцию доступной для чтения и записи.

Еще одним большим преимуществом разделения методов «чтение-запись» и «только для чтения» является то, что мы можем направлять их на разные узлы базы данных, как объясняется в этой статье.

image-loader.svg

Таким образом, мы можем масштабировать трафик только для чтения, увеличивая количество узлов-реплик. Потрясающе, правда?

Заключение

Аннотация Spring Transactional очень удобна, когда речь идет об определении границ транзакций бизнес-методов.

Хотя значения атрибутов по умолчанию были выбраны правильно, хорошей практикой является предоставление настроек как на уровне класса так и на уровне метода, чтобы разделить варианты использования на нетранзакционные, транзакционные, только для чтения и чтения-записи.

Данный материал подготовили для будущих студентов нового потока курса «Разработчик на Spring Framework», а всех желающих приглашаем на бесплатный открытый урок на тему «Правильный DAO на Spring JDBC». На этом занятии рассмотрим, как использовать всю мощь нативного SQL и при этом написать безопасное, поддерживаемое и тестируемое DAO с использованием Spring JDBC.

© Habrahabr.ru