Всем добрый день!

Что ж, конец месяца у нас всегда интенсивные, вот и тут остался всего день до старта второго потока курса «Разработчик на Spring Framework» — замечательного и интересного курса, который ведёт не менее прекрасный и злой Юрий (как его называют некоторые студент за уровень требований в ДЗ), так что давайте рассмотрим ещё один материал, который мы подготовили для вас.

Поехали.

Введение

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

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



Самый важный аспект для разработчиков — это понять как реализовать управление транзакциями в приложении наилучшим образом. Поэтому давайте рассмотрим различные варианты.

Способы управления транзакциями

Транзакции могут управляться следующими способами:

1. Программное управление путем написания пользовательского кода

Это старый способ управления транзакциями.

EntityManagerFactory factory = Persistence.createEntityManagerFactory("PERSISTENCE_UNIT_NAME");                                       EntityManager entityManager = entityManagerFactory.createEntityManager();                   
Transaction transaction = entityManager.getTransaction()                  
try                                       
{  
   transaction.begin();                   
   someBusinessCode();                    
   transaction.commit();  
}                  
catch(Exception ex)                   
{                     
   transaction.rollback();  
   throw ex;                  
}

Плюсы:

  • Границы транзакции очевидны в коде.

Минусы:

  • Он повторяющийся и подвержен ошибкам
  • Любая ошибка может иметь очень большое влияние.
  • Нужно написать множество шаблонов, также, если вы хотите вызвать другой метод из этого метода, вам снова нужно управлять им из кода.

2. Использование Spring для управления транзакциями

Spring поддерживает два типа управления транзакциями

1. Программное управление транзакциями: Вы должны управлять транзакциями с помощью программирования. Это способ достаточно гибкий, но его сложно поддерживать.

2. Декларативное управление транзакциями: Вы отделяете управление транзакциями от бизнес-логики. Вы используете только аннотации в конфигурации на основе XML для управления транзакциями.

Мы настоятельно рекомендуем использовать декларативные транзакции. Если вы хотите узнать причины, тогда читайте дальше, иначе переходите сразу к разделу Декларативное управление транзакциями, если хотите реализовать этот вариант.

Теперь давайте рассмотрим каждый подход детально.

2.1. Программное управление транзакциями:

Фреймворк Spring предоставляет два средства для программного управления транзакциями.

a. Использование TransactionTemplate (рекомендовано командой Spring):

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

Обратите внимание, что фрагменты кода взяты из Spring Docs.

Файл Context Xml:

<!-- Initialization for data source -->   
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">      <property name="driverClassName" value="com.mysql.jdbc.Driver"/>      
<property name="url" value="jdbc:mysql://localhost:3306/TEST"/>      
<property name="username" value="root"/>      
<property name="password" value="password"/>   
</bean>
<!-- Initialization for TransactionManager -->   
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">    
<property name="dataSource"  ref="dataSource" />  
</bean>
<!-- Definition for ServiceImpl bean -->   
<bean id="serviceImpl" class="com.service.ServiceImpl">    
<constructor-arg ref="transactionManager"/>   
</bean>

Класс Service:

public class ServiceImpl implements Service
{       
 private final TransactionTemplate transactionTemplate;
 // используйте инъекцию в конструктор чтобы предоставить PlatformTransactionManager   
 public ServiceImpl(PlatformTransactionManager transactionManager)
 {    
this.transactionTemplate = new TransactionTemplate(transactionManager);  
 }
// параметры транзакции здесь могут быть установлены явно, если это необходимо, обеспечивая больше контроля
 // Также мы можем сделать это через xml файл              
 this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);                   this.transactionTemplate.setTimeout(30); //30 секунд    
 /// и так далее

 public Object someServiceMethod()
 {       
   return transactionTemplate.execute(new TransactionCallback()
   {
    // код в этом методе выполняется в транзакционном контексте         
    public Object doInTransaction(TransactionStatus status)
    {               
    updateOperation1();    
       return resultOfUpdateOperation2();   
    }
  });  
}}

Если нет возвращаемого значения, используйте удобный класс TransactionCallbackWithoutResult с анонимным классом, как показано ниже:

transactionTemplate.execute(new TransactionCallbackWithoutResult()
{   
protected void doInTransactionWithoutResult(TransactionStatus status)
{      
 updateOperation1();     
  updateOperation2(); 
}
});

  • Экземпляры класса TransactionTemplate потокобезопасные, поэтому поддерживают не все диалоговые состояния.
  • Экземпляры TransactionTemplate тем не менее поддерживают конфигурационное состояние, поэтому, если классу необходимо использовать TransactionTemplate с разными настройками (например, другой уровень изоляции), то вам нужно создать два различных экземпляра TransactionTemplate, хотя в некоторых классах может использоваться один экземпляр TransactionTemplate.

b. Использование реализации PlatformTransactionManager напрямую:

Давайте снова посмотрим на эту опцию в коде.

<!-- Initialization for data source --> 
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">   
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>    
<property name="url" value="jdbc:mysql://localhost:3306/TEST"/>     
<property name="username" value="root"/>     
<property name="password" value="password"/> 
</bean>
<!-- Initialization for TransactionManager -->  
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">     
<property name="dataSource"  ref="dataSource" />      
</bean>
public class ServiceImpl implements Service
{   
private PlatformTransactionManager transactionManager;
public void setTransactionManager( PlatformTransactionManager transactionManager)
{   
  this.transactionManager = transactionManager; 
}
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// Явное указание имени транзакции - это то, что может быть сделано только программно
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = txManager.getTransaction(def);
try
{   
// выполняем вашу бизнес-логику здесь
}
catch (Exception ex)
{   
txManager.rollback(status);  
throw ex;
}
txManager.commit(status);
}

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

Выбор между Программным и Декларативным управлением транзакциями:

  • Программное управление транзакциями — хороший выбор только в том случае, если у вас небольшое количество транзакционных операций. (В большинстве случаев это не транзакции.)
  • Имя транзакции может быть явно установлено только при Программном управлении транзакциями.
  • Программное управление транзакциями должно быть использовано когда вы хотите явно контролировать управление транзакциями.
  • С другой стороны, если в вашем приложении содержаться многочисленные транзакционные операции, стоит использовать декларативное управление.
  • Декларативное управление не позволяет управлять транзакциями в бизнес-логике и его несложно настроить.

2.2. Декларативные транзакции (Обычно используется почти во всех сценариях любого веб-приложения)

Шаг 1: Определите менеджер транзакций в контекстном xml файле вашего spring-приложения.

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"/>
<tx:annotation-driven transaction-manager="txManager"/>

Шаг 2: Включите поддержку аннотаций, добавив запись в контекстном xml файле вашего spring-приложения.

ИЛИ добавьте @EnableTransactionManagement в ваш конфигурационный файл, как показано ниже:

@Configuration
@EnableTransactionManagement
public class AppConfig
{
...
}

Spring рекомендует аннотировать только конкретные классы (и методы конкретных классов) с аннотацией @Transactional в сравнении с аннотирующими интерфейсами.

Причина этого заключается в том, что вы помещаете аннотацию на уровень интерфейса, и если вы используете прокси-классы (proxy-target-class = «true») или сплетающий аспект (mode = «aspectj»), тогда параметры транзакции не распознаются инфраструктурой проксирования и сплетения, например Транзакционное поведение не будет применяться.

Шаг 3: Добавьте аннотацию @Transactional в класс (метод класса) или интерфейс (метод интерфейса).

<tx:annotation-driven proxy-target-class="true">

Конфигурация по умолчанию: proxy-target-class="false"

  • Аннотация @Transactional может быть помещена перед определением интерфейса, метода интерфейса, определением класса или публичным методом класса.
  • Если вы хотите, чтобы некоторые методы класса (помеченные аннотацией @Transactional) имели разные настройки атрибутов, такие как уровень изоляции или уровень распространения, разместите аннотацию на уровне метода, чтобы переопределить настройки атрибутов уровня класса.
  • В режиме прокси (который установлен по-умолчанию) могут быть перехвачены только “внешние“ вызовы метода, идущие через прокси. Это означает, что “самостоятельный вызов”, например метод в целевом объекте, вызывающий какой-либо другой метод целевого объекта, не приведет к фактической транзакции во время выполнения даже если вызываемый метод помечен с @Transactional.

Теперь давайте разберем разницу между атрибутами аннотации @Transactional

@Transactional (isolation=Isolation.READ_COMMITTED)

  • По умолчанию установлено Isolation.DEFAULT
  • В большинстве случаев, вы будете использовать настройки по-умолчанию до тех пор, пока у вас не появится особые требования.
  • Сообщает менеджеру транзакции (tx), что для текущего tx должен использоваться следующий уровень изоляции. Должен быть установлен в точке, откуда начинается tx, потому что мы не можем изменить уровень изоляции после запуска tx.

DEFAULT: Использовать уровень изоляции установленный по умолчанию в базовой базе данных.

READ_COMMITTED (чтение фиксированных данных): Постоянная, указывающая, что “грязное” чтение предотвращено; могут возникать неповторяющееся чтение и фантомное чтение.

READ_UNCOMMITTED (чтение незафиксированных данных): Этот уровень изоляции указывает, что транзакция может считывать данные, которые еще не удалены другими транзакциями.

REPEATABLE_READ (повторяемость чтения): Постоянная, указывающая на то, что “грязное” чтение и неповторяемое чтение предотвращаются; может появляться фантомное чтение.

SERIALIZABLE (упорядочиваемость): Постоянная, указывающая, что “грязное” чтение, неповторяемое чтение и фантомное чтение предотвращены.

Что означают эти жаргонизмы: “грязное” чтение, фантомное чтение или повторяемое чтение?

  • “Грязное” чтение (Dirty Read): транзакция «A» производит запись. Между тем, транзакция «B» считывает ту же самую запись до завершения транзакции A. Позже транзакция A решает откатится, и теперь у нас есть изменения в транзакции B, которые несовместимы. Это грязное чтение. Транзакция B работала на уровне изоляции READ_UNCOMMITTED, поэтому она могла считывать изменения, внесенные транзакцией A до того, как транзакция завершилась.
  • Неповторяющееся чтение (Non-Repeatable Read): транзакция «A» считывает некоторые записи. Затем транзакция «B» записывает эту запись и фиксирует ее. Позже транзакция A снова считывает эту же запись и может получить разные значения, поскольку транзакция B вносила изменения в эту запись и фиксировала их. Это неповторяющееся чтение.
  • Фантомные чтение (Phantom Read): транзакция «A» читает ряд записей. Между тем, транзакция «B» вставляет новую запись в этот же ряд, что и транзакция A. Позднее транзакция A снова считывает тот же диапазон и также получит запись, которую только что вставила транзакция B. Это фантомное чтение: транзакция извлекала ряд записей несколько раз из базы данных и получала разные результирующие наборы (содержащие фантомные записи).

@Transactional(timeout=60)

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

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

@Transactional(propagation=Propagation.REQUIRED)

Если не указано, распространяющееся поведение по умолчанию — REQUIRED.

Другие варианты: REQUIRES_NEW, MANDATORY, SUPPORTS, NOT_SUPPORTED, NEVER и NESTED.

REQUIRED

Указывает, что целевой метод не может работать без активного tx. Если tx уже запущен до вызова этого метода, то он будет продолжаться в том же tx, или новый tx начнется вскоре после вызова этого метода.

REQUIRES_NEW

  • Указывает, что новый tx должен запускаться каждый раз при вызове целевого метода. Если уже идет tx, он будет приостановлен, прежде чем запускать новый.

MANDATORY

  • Указывает, что для целевого метода требуется активный tx. Если tx не будет продолжаться, он не сработает, выбросив исключение.

SUPPORTS

  • Указывает, что целевой метод может выполняться независимо от tx. Если tx работает, он будет участвовать в том же tx. Если выполняется без tx, он все равно будет выполняться, если ошибок не будет.

  • Методы, которые извлекают данные, являются лучшими кандидатами для этой опции.

NOT_SUPPORTED

  • Указывает, что целевой метод не требует распространения контекста транзакции.
  • В основном те методы, которые выполняются в транзакции, но выполняют операции с оперативной памятью, являются лучшими кандидатами для этой опции.

NEVER

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

@Transactional (rollbackFor=Exception.class)

Значение по умолчанию: rollbackFor=RunTimeException.class

В Spring все классы API бросают RuntimeException, это означает, что если какой-либо метод не выполняется, контейнер всегда откатывает текущую транзакцию.

Проблема заключается только в проверенных исключениях. Таким образом, этот параметр можно использовать для декларативного отката транзакции, если происходит Checked Exception.

@Transactional (noRollbackFor=IllegalStateException.class)

Указывает, что откат не должен происходить, если целевой метод вызывает это исключение.

Теперь последним, но самым важным шагом в управлении транзакциями является размещение аннотации @Transactional. В большинстве случаев возникает путаница, где должна размещаться аннотация: на сервисном уровне или на уровне DAO?

@Transactional: Сервисный или DAO уровень?

Сервис — лучшее место для размещения @Transactional, сервисный уровень должен содержать поведение варианта использования на уровне детализации для взаимодействия пользователя, которое логически переходит в транзакцию.

Существует много CRUD-приложений, у которых нет существенной бизнес-логики, имеющих сервисный уровень, который просто передает данные между контроллерами и объектами доступа к данным, что не является полезным. В этих случаях мы можем поместить аннотацию транзакции на уровень DAO.

Поэтому на практике вы можете поместить их в любом месте, это зависит от вас.

Кроме того, если вы поместите @Transactional в уровень DAO и если ваш уровень DAO будет повторно использоваться разными службами, тогда будет сложно разместить его на уровне DAO, так как разные службы могут иметь разные требования.

Если ваш сервисный уровень извлекает объекты с помощью Hibernate, и, допустим, у вас есть ленивые инициализации в определении объекта домена, тогда вам нужно открыть транзакцию на сервисном уровне, иначе вам придется столкнуться с LazyInitializationException, брошенным ORM.

Рассмотрим другой пример, когда ваш уровень обслуживания может вызывать два разных метода DAO для выполнения операций БД. Если ваша первая операция DAO завершилась неудачей, остальные две могут быть переданы, и вы закончите несогласованное состояние БД. Аннотирование на сервисном уровне может спасти вас от таких ситуаций.

Надеюсь, эта статья вам помогла.

THE END

Всегда интересно увидеть ваш комментарии или вопросы.

Комментарии (2)


  1. Djaler
    29.11.2018 15:19
    +1

    Я, конечно, извиняюсь, но в 2018 году конфигурировать через XML как-то не очень.

    Upd. Ну да, оригиналу статьи то 2.5 года уже


    1. fori1ton
      01.12.2018 14:58

      Да и 2.5 года назад XML-конфигурация была уже малость старомодной. Spring 4.0 вышел в 2013, Spring Boot — в 2014. «Зато XML конфигурацию можно править без перекомпиляции приложения», скажут некоторые, и будут правы. Но неужели такая необходимость возникает часто? Я помню ровно одну историю (кажется, она была на Хабре) о том как человек правил XML-конфиги на проде, диктуя инструкции по телефону саппортёру, который в этом вашем спринге не разбирался. Кто-нибудь может накидать ещё подобных историй, в которых XML-конфиг имел бы реальные преимущества перед Java-конфигом или автоконфигурацией?