Вы можете использовать это руководство, чтобы получить простое и практическое понимание того, как работает управление транзакциями в Spring (Спринге) с помощью аннотации @Transacional .

Единственное необходимое условие? Вы должны иметь приблизительное представление об ACID, то есть о том, что такое транзакции баз данных и зачем их использовать. Также здесь не рассматриваются распределенные транзакции или реактивные транзакции, хотя общие принципы, с точки зрения Spring всё же применимы.

Введение

В этом руководстве вы узнаете об основных столпах "transaction abstraction framework"("структуры абстракции транзакций" - запутанный термин, не так ли?) Spring core - описанных с большим количеством примеров кода:

  • @Transactional (декларативное управление транзакциями) против программного управления транзакциями.

  • Физические и логические транзакции.

  • Интеграция @Transactional Spring и JPA / Hibernate.

  • Интеграция @Transactional Spring и Spring Boot или Spring MVC.

  • Откаты, прокси, общие проблемы и многое другое.

В отличие, скажем, от официальной документации по Spring это руководство не запутает вас, погрузившись сразу в тему Spring-first.

Вместо этого вы будете изучать управление транзакциями Spring нетрадиционным способом: с нуля, шаг за шагом. Это означает, что вы начнёте со старого доброго управления транзакциями JDBC.

Почему?

Потому что всё, что делает Spring, построено на этих самых основах JDBC (соединение с базами данных на Java). И вы сэкономите кучу времени при работе с аннотацией @Transactional от Spring, если усвоите эти начала.

Как работает обычное Управление транзакциями JDBC

Если вы думаете пропустить этот раздел, не зная транзакций JDBC изнутри: не стоит.

Как запускать, фиксировать или откатывать транзакции JDBC

Первый важный вывод заключается в следующем: Не имеет значения, используете ли вы аннотацию @Transactional от Spring, обычный Hibernate, jOOQ или любую другую библиотеку баз данных.

В конечном счёте, все они делают одно и то же - открывают и закрывают (назовём это "управлением") транзакции базы данных. Обычный код управления транзакциями JDBC выглядит следующим образом:

import java.sql.Connection;

Connection connection = dataSource.getConnection(); // (1)

try (connection) {
    connection.setAutoCommit(false); // (2)
    // выполнить несколько SQL-запросов...
    connection.commit(); // (3)

} catch (SQLException e) {
    connection.rollback(); // (4)
}
  • Для запуска транзакций необходимо подключение к базе данных. DriverManager.getConnection(url, user, password) тоже подойдёт, хотя в большинстве корпоративных приложений вы будете иметь настроенный источник данных и получать соединения из него.

  • Это единственный способ "начать" транзакцию базы данных в Java, даже несмотря на то, что название может звучать немного странно. setAutoCommit(true) гарантирует, что каждый SQL-оператор будет автоматически завёрнут в собственную транзакцию, а setAutoCommit(false) - наоборот: Вы являетесь хозяином транзакции (транзакций), и Вам придётся начать вызывать commit и друзей. Обратите внимание, что флаг autoCommit действует в течение всего времени, пока ваше соединение открыто, что означает, что вам нужно вызвать метод только один раз, а не несколько.

  • Давайте зафиксируем нашу транзакцию...

  • Или откатим наши изменения, если произошло исключение.

Да, эти 4 строки - это (в упрощенном виде) всё, что Spring делает всякий раз, когда вы используете аннотацию @Transactional . В следующей главе вы узнаете, как это работает. Но прежде чем мы перейдём к этому, вам нужно узнать ещё кое-что.

(Небольшое замечание для умников: Библиотеки пулов соединений, такие как HikariCP, могут переключать режим автокоммита автоматически, в зависимости от конфигурации. Но это уже продвинутая тема).

Как использовать уровни изоляции JDBC и точки сохранения

Если вы уже играли с аннотацией @Transactional от Spring, вы могли столкнуться с чем-то подобным:

@Transactional(
  propagation=TransactionDefinition.NESTED,
  isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED
)

Мы рассмотрим вложенные транзакции Spring и уровни изоляции позже более подробно, но опять же полезно знать, что все эти параметры сводятся к следующему, основному коду JDBC:

import java.sql.Connection;

// isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED

connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); // (1)

// propagation=TransactionDefinition.NESTED

Savepoint savePoint = connection.setSavepoint(); // (2)
...
connection.rollback(savePoint);
  • Вот как Spring устанавливает уровни изоляции для соединения с базой данных. Не такое уж хитросплетённый, не так ли?

  • Вложенные транзакции в Spring - это просто точки сохранения JDBC / базы данных. Если вы не знаете, что такое точка сохранения, посмотрите, например, это руководство. Обратите внимание, что поддержка точек сохранения зависит от вашего драйвера JDBC/базы данных.

Как работает управление транзакциями в Spring или Spring boot

Поскольку теперь у вас есть хорошее понимание транзакций JDBC, давайте посмотрим, как Spring core управляет транзакциями. Здесь всё применимо 1:1 к Spring boot и Spring MVC, но об этом чуть позже...

Что на самом деле представляет собой управление транзакциями Spring или его (довольно запутанно названная) структура абстракции транзакций?

Помните, что управление транзакциями просто означает: Как Spring запускает, фиксирует или откатывает транзакции JDBC? Звучит ли это как-то знакомо?

Вот в чём загвоздка: В то время как в обычном JDBC у вас есть только один способ (setAutocommit(false)) управлять транзакциями, Spring предлагает вам множество различных, более удобных способов добиться того же самого.

Как использовать "программное управление транзакциями Spring" (Programmatic Transaction Management)?

Первый, но довольно редко используемый способ определения транзакций в Spring - программный: Либо через транзакционный шаблон TransactionTemplate, либо непосредственно через PlatformTransactionManager. С точки зрения кода это выглядит следующим образом:

@Service
public class UserService {

    @Autowired
    private TransactionTemplate template;

    public Long registerUser(User user) {
        Long id = template.execute(status ->  {
            // выполнить некоторый SQL, который, например,
            // вставляет пользователя в базу данных 
            // и возвращает автогенерированный идентификатор
          return id;
        });
    }
}

По сравнению с обычным примером JDBC:

  • Вам не нужно возиться с открытием и закрытием соединений с базой данных самостоятельно (try-finally). Вместо этого вы используете обратные вызовы транзакций.

  • Вам также не нужно ловить SQLExceptions, поскольку Spring преобразует эти исключения в исключения времени выполнения (runtime exceptions).

  • Кроме того, вы лучше интегрируетесь в экосистему Spring. TransactionTemplate будет использовать TransactionManager внутри, который будет использовать источник данных. Все это бины, которые вы должны указать в конфигурации контекста Spring, но о которых вам больше не придётся беспокоиться в дальнейшем.

Хотя это и считается незначительным улучшением, программное управление транзакциями - это не то, чем в основном занимается транзакционная структура (transaction framework) Spring. Вместо этого, все дело в декларативном управлении транзакциями. Давайте выясним, что это такое.

Как использовать декларативное управление транзакциями XML от Spring?

В те времена, когда конфигурация XML была нормой для проектов Spring, вы могли конфигурировать транзакции непосредственно в XML. За исключением нескольких старых корпоративных проектов, вы больше не встретите такой подход в природе, поскольку он был вытеснен гораздо более простой аннотацией @Transactional .

В этом руководстве мы не будем подробно останавливаться на конфигурации XML, но вы можете использовать этот пример в качестве отправной точки, чтобы углубиться в него - при необходимости (он взят прямо из официальной документации Sprnig):

<!-- транзакционный совет (advice) (что "происходит"; см. <aop:advisor/> бин ниже) -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!-- семантика транзакций... -->
        <tx:attributes>
            <!--все методы, начинающиеся с 'get', доступны только для чтения -->
            <tx:method name="get*" read-only="true"/>
            <!-- другие методы используют настройки транзакции по умолчанию (см. ниже) -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

Вы определяете advice AOP (Aspect Oriented Programming, аспектно-ориентированное программировие) с помощью приведенного выше блока XML, который затем можно применить к бин UserService следующим образом:

<aop:config>
    <aop:pointcut id="userServiceOperation" expression="execution(* x.y.service.UserService.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="userServiceOperation"/>
</aop:config>

<bean id="userService" class="x.y.service.UserService"/>

Тогда ваш бин UserService будет выглядеть следующим образом:

public class UserService {

    public Long registerUser(User user) { 
        // выполняем SQL, который, например.
        // вставляет пользователя в базу данных 
        // и извлекает автогенерированный id
      return id;
    }
}

С точки зрения кода Java, этот декларативный подход к транзакциям выглядит намного проще, чем программный подход. Но он приводит к большому количеству сложного, многословного XML с конфигурациями указателей и советников (advisor).

Таким образом, это приводит к вопросу: Есть ли лучший способ управления декларативными транзакциями вместо XML? Да, есть: аннотация @Transacional .

Как использовать аннотацию @Transactional Spring (декларативное управление транзакциями, Declarative Transaction Management)

Теперь давайте посмотрим, как обычно выглядит современное управление транзакциями в Spring:

public class UserService {

    @Transactional
    public Long registerUser(User user) {
        // выполнить некоторый SQL, который, например,
        // вставляет пользователя в базу данных 
        // и извлекает автогенерированный id
        // userDao.save(user);
        return id;
    }
}

Как это возможно? Больше нет конфигурации XML, а также нет необходимости в другом коде. Вместо этого вам теперь нужно сделать две вещи:

  • Убедитесь, что ваша Configuration Spring сопровождена аннотацией @EnableTransactionManagement Spring boot это будет сделано автоматически).

  • Убедитесь, что вы указали менеджер транзакций в вашей Configuration Spring (это необходимо сделать в любом случае).

И тогда Spring покажет себя достаточно умным, чтобы явно обрабатывать транзакции для вас: Любой публичный public метод бин, который вы сопровождаете аннотацией @Transactional , будет выполняться внутри транзакции базы данных (обратите внимание: есть некоторые подводные камни).

Итак, чтобы аннотация @Transactional заработала, всё, что вам нужно сделать, это следующее:

@Configuration
@EnableTransactionManagement
public class MySpringConfig {

    @Bean
    public PlatformTransactionManager txManager() {
        return yourTxManager; // подробнее об этом позже
    }

}

Теперь, когда я говорю, что Spring явно обрабатывает транзакции для вас. Что это означает на самом деле?

Вооружившись знаниями из примера транзакций JDBC, приведённый выше код @Transactional UserService переводится (упрощенно) непосредственно следующим образом:

public class UserService {

    public Long registerUser(User user) {
        Connection connection = dataSource.getConnection(); // (1)
        try (connection) {
            connection.setAutoCommit(false); // (1)

            // выполнить некоторый SQL, который, например.
            // вставляет пользователя в базу данных 
            // и извлекает автогенерированный идентификатор
            // userDao.save(user); <(2)

            connection.commit(); // (1)
        } catch (SQLException e) {
            connection.rollback(); // (1)
        }
    }
}
  • Все это просто стандартное открытие и закрытие соединения JDBC. Это то, что транзакционная аннотация Spring делает для вас автоматически, без необходимости писать её явно.

  • Это ваш собственный код, сохраняющий пользователя через DAO или что-то подобное.

Этот пример может выглядеть немного магическим, но давайте посмотрим, как Spring вставляет этот код соединения для вас.

CGlib и JDK прокси - @Transactional под прикрытием

Spring не может переписать ваш класс Java, как я сделал выше, чтобы вставить код подключения (если только вы не используете продвинутые техники, такие как плетение байткода (bytecode weaving), но мы пока опускаем это).

Ваш метод registerUser() действительно просто вызывает userDao.save(user), и нет способа изменить это на лету.

Но у Spring есть преимущество. По своей сути это IoC container (контейнер инверсии контроля). Он создаёт для вас экземпляр UserService и обеспечивает автоподключение этого UserService к любому другому бины, которому нужен этот UserService.

Теперь, когда вы используете @Transactional над бинами, Spring использует маленькую хитрость. Он не просто инстанцирует UserService, но и транзакционный прокси этого UserService.

Он делает это с помощью метода под названием proxy-through-subclassing с помощью библиотеки Cglib. Существуют и другие способы построения прокси (например, Dynamic JDK proxies), но пока оставим это на потом.

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

Как видно из этой диаграммы, у конкретного прокси есть одна задача.

  • Открытие и закрытие соединений/транзакций с базой данных.

  • А затем делегирование настоящему UserService, тому, который вы написали.

  • А другие бины, такие как ваш UserRestController, никогда не узнают, что они разговаривают с прокси, а не с настоящим.

Короткий тест

Посмотрите на следующий исходный код и скажите мне, какой тип UserService автоматически конструирует Spring, предполагая, что он помечен @Transactional на уровне класса или имеет на уровне метода внутри класса конфигурации.

@Configuration
@EnableTransactionManagement
public static class MyAppConfig {

    @Bean
    public UserService userService() {  // (1)
        return new UserService();
    }
}
Ответ

Правильно. Spring создаёт здесь динамический CGLib-прокси вашего класса UserService, который может открывать и закрывать транзакции базы данных для вас. Вы или любые другие бины даже не заметят, что это не ваш UserService, а прокси, обертывающий ваш UserService.

Для чего вам нужен менеджер транзакций (например, PlatformTransactionManager)?

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

Ваш UserService проксируется на лету, и прокси управляет транзакциями для вас. Но не сам прокси управляет всем этим транзакционным состоянием (открыть, зафиксировать, закрыть), прокси делегирует эту работу менеджеру транзакций.

Spring предлагает вам интерфейс PlatformTransactionManager / TransactionManager, который по умолчанию поставляется с парой удобных реализаций. Одна из них - менеджер транзакций источника данных.

Он делает то же самое, что вы делали до сих пор для управления транзакциями, но сначала давайте рассмотрим необходимую конфигурацию Spring:

@Bean
public DataSource dataSource() {
    return new MysqlDataSource(); // (1)
}

@Bean
public PlatformTransactionManager txManager() {
    return new DataSourceTransactionManager(dataSource()); // (2)
}
  • Здесь вы создаёте источник данных, специфичный для базы данных или для пула соединений. В данном примере используется MySQL.

  • Здесь вы создаёте свой менеджер транзакций, которому нужен источник данных, чтобы иметь возможность управлять транзакциями.

Всё просто. Все менеджеры транзакций имеют методы типа "doBegin" (для запуска транзакции) или "doCommit", которые выглядят следующим образом - взяты прямо из исходного кода Spring с некоторым упрощением:

public class DataSourceTransactionManager implements PlatformTransactionManager {

    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        Connection newCon = obtainDataSource().getConnection();
        // ...
        con.setAutoCommit(false);
        // Да, вот так.!
    }

    @Override
    protected void doCommit(DefaultTransactionStatus status) {
        // ...
        Connection connection = status.getTransaction().getConnectionHolder().getConnection();
        try {
            con.commit();
        } catch (SQLException ex) {
            throw new TransactionSystemException("Could not commit JDBC transaction", ex);
        }
    }
}

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

Учитывая это, давайте расширим наш рисунок сверху:

Подведем итоги:

  • Если Spring обнаруживает аннотацию @Transactional на бины, он создаёт динамический прокси этого бины.

  • Прокси имеет доступ к менеджеру транзакций и будет просить его открывать и закрывать транзакции/соединения.

  • Сам менеджер транзакций будет просто делать то, что вы делали в разделе "Простой JDBC": Управлять старым добрым соединением JDBC.

В чём разница между физическими и логическими транзакциями?

Представьте следующие два класса транзакций.

@Service
public class UserService {

    @Autowired
    private InvoiceService invoiceService;

    @Transactional
    public void invoice() {
        invoiceService.createPdf();
        // отправка счёта по электронной почте и т.д...
    }
}

@Service
public class InvoiceService {

    @Transactional
    public void createPdf() {
        // ...
    }
}

UserService имеет транзакционный метод invoice(). Который вызывает другой транзакционный метод, createPdf() на InvoiceService.

Теперь, с точки зрения транзакций базы данных, это должна быть всего лишь одна транзакция базы данных. (Помните: getConnection().setAutocommit(false).commit()). Spring называет это физической транзакцией, хотя сначала это может показаться немного запутанным.

Однако со стороны Spring происходит две логические транзакции: Первая - в UserService, вторая - в InvoiceService. Spring должен быть достаточно умным, чтобы знать, что оба метода @Transacional должны использовать в основе одну и ту же физическую транзакцию базы данных.

Как изменится ситуация при следующем изменении InvoiceService?

@Service
public class InvoiceService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createPdf() {
        // ...
    }
}

Изменение режима распространения (propagation) на requires_new говорит Spring, что createPDF() должна выполняться в собственной транзакции, независимо от любой другой, уже существующей транзакции. Вспоминая раздел данного руководства, посвященный простой Java, видели ли вы способ "разделить" транзакцию пополам? Я тоже.

Это означает, что ваш код будет открывать два (физических) соединения/транзакции с базой данных. (Снова: getConnection() x2. setAutocommit(false) x2. commit() x2). Spring теперь должен быть достаточно умным, чтобы две логические транзакции (invoice()/createPdf()) теперь также отображались на две разные физические транзакции базы данных.

Итак, подведем итоги:

  • Физические транзакции: это ваши фактические транзакции JDBC.

  • Логические транзакции: это (потенциально вложенные) аннотированные @Transactional методы Spring.

Это подводит нас к более подробному рассмотрению режимов распространения.

What are @Transactional Propagation Levels used for?

Изучая исходный код Spring, вы найдете множество уровней или режимов распространения, которые можно подключить к методу @Transactional.

  @Transactional(propagation = Propagation.REQUIRED)

  // или

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  // и т.д.

Полный перечень:

  • REQUIRED

  • SUPPORTS

  • MANDATORY

  • REQUIRES_NEW

  • NOT_SUPPORTED

  • NEVER

  • NESTED

Упражнение:

В разделе "Обычный JDBC" я показал вам всё, на что способен JDBC, когда речь идёт о транзакциях. Потратьте минуту, чтобы подумать о том, что каждый отдельный режим распространения в Spring в итоге НА САМОМ ДЕЛЕ делает с вашим источником данных или, скорее, с вашим соединением JDBC.

Затем посмотрите на следующие ответы.

Ответы
  • Required (по умолчанию): Моему методу нужна транзакция, либо откройте ее для меня, либо используйте существующую → getConnection().setAutocommit(false).commit()

  • Supports: Мне не важно, открыта транзакция или нет, я могу работать в любом случае → не имеет отношения к JDBC

  • Mandatory: Я не собираюсь открывать транзакцию сам, но я буду плакать, если никто другой не откроет её → не имеет отношения к JDBC

  • Require_new: Я хочу полностью собственную транзакцию → getConnection().setAutocommit(false).commit()

  • Not_Supported: Мне очень не нравятся транзакции, я даже попытаюсь приостановить текущую, запущенную транзакцию → ничего общего с JDBC

  • Never: Я буду плакать, если кто-то другой запустит транзакцию → не имеет отношения к JDBC

  • Nested: Это звучит так сложно, но мы просто говорим о точках сохранения! → connection.setSavepoint()

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

Посмотрите на этот пример:

public class UserService {

     @Transactional(propagation = Propagation.MANDATORY)
     public void myMethod() {
        // выполнить sql
     }

}

В этом случае Spring будет ожидать, что транзакция будет открыта, когда вы вызовете myMethod() класса UserService. Он не открывает её сам, вместо этого, если вы вызовете этот метод без предварительно существующей транзакции, Spring выбросит исключение. Помните об этом как о дополнительных нюансах для "логической обработки транзакций".

Для чего используются @Transactional уровни изоляции?

Это почти вопрос с подвохом, но что произойдет, если вы настроите аннотацию @Transactional таким образом?

@Transactional(isolation = Isolation.REPEATABLE_READ)

Да, это просто приводит к этому:

connection
    .setTransactionIsolation(
            Connection.TRANSACTION_REPEATABLE_READ
    );

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

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

Самый распространенный "подводный камень" @Transactional

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

@Service
public class UserService {

    @Transactional
    public void invoice() {
        createPdf();
        // отправка счета по электронной почте и т.д.
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createPdf() {
        // ...
    }
}

У вас есть класс UserService с транзакционным методом invoice(). Который вызывает createPDF(), который также является транзакционным.

Сколько физических транзакций, по вашему мнению, будет открыто, когда кто-то вызовет invoice()?

Нет, ответ будет не два, а один. Почему?

Давайте вернемся к разделу "Прокси" этого руководства. Spring создает для вас транзакционный прокси UserService, но как только вы оказываетесь внутри класса UserService и вызываете другие внутренние методы, прокси больше не задействован. Это означает, что новой транзакции не будет.

Давайте посмотрим на это на рисунке:

Есть несколько приёмов (например, самоинъекция), которые можно использовать, чтобы обойти это ограничение. Но главный вывод: всегда помните о границах транзакций прокси.

Как использовать @Transactional сo Spring boot или Spring MVC

До сих пор мы говорили только об обычном, базовом Spring. Но как насчет Spring boot? Или Spring MVC? Обрабатывают ли они транзакции по-другому?

Короткий ответ таков: Нет.

В обоих фреймворках (а точнее: во всех фреймворках экосистемы Spring) вы всегда будете использовать аннотацию @Transactional в сочетании с менеджером транзакций и аннотацией @EnableTransactionManager пути не существует.

Однако единственное отличие Spring заключается в том, что он автоматически устанавливает аннотацию @EnableTransactionManager и создает PlatformTransactionManager для вас - с помощью автоконфигураций JDBC. Подробнее об автоконфигурациях здесь.

Как Spring обрабатывает откат (и политики отката по умолчанию)

Раздел об откатах Spring будет рассмотрен в следующей редакции этого руководства.

Как работает управление транзакциями Spring и JPA / Hibernate

Цель: синхронизация @Transactional от Spring и JPA / Hibernate

В какой-то момент вы захотите, чтобы ваше приложение Spring интегрировалось с другой библиотекой баз данных, такой как Hibernate (популярная реализация JPA) или jOOQ и т.д.

В качестве примера рассмотрим простой Hibernate (примечание: не имеет значения, используете ли вы Hibernate напрямую, или Hibernate через JPA).

Переписывание UserService из предыдущей версии в Hibernate будет выглядеть следующим образом:

public class UserService {

    @Autowired
    private SessionFactory sessionFactory; // (1)

    public void registerUser(User user) {

        Session session = sessionFactory.openSession(); // (2)

        // давайте откроем транзакцию. помните о setAutocommit(false)!
        session.beginTransaction();

        // сохранить == вставить (save == insert) наших объектов
        session.save(user);

        // и закрепить его
        session.getTransaction().commit();

        // закрыть сессию == наше соединение СБДЯ
        session.close();
    }
}
  • Это обычная, старая SessionFactory Hibernate, точка входа для всех запросов в Hibernate.

  • Ручное управление сессиями (читай: соединениями с базой данных) и транзакциями с помощью API Hibernate.

Однако в приведённом выше коде есть одна огромная проблема:

  • Hibernate не будет знать об аннотации @Transactional от Spring.

  • @Transactional Spring не будет знать ничего о транзакциях Hibernate.

Но нам бы хотелось, чтобы Spring и Hibernate интегрировались без проблем, то есть знали о транзакциях друг друга.

В простом коде:

@Service
public class UserService {

    @Autowired
    private SessionFactory sessionFactory; // (1)

    @Transactional
    public void registerUser(User user) {
        sessionFactory.getCurrentSession().save(user); // (2)
    }

}
  • Тот же SessionFactory, что и раньше

  • Но больше никакого ручного управления состоянием. Вместо этого getCurrentSession() и @Transactional синхронизируются.

    Как этого добиться?

Использование HibernateTransactionManager

Существует очень простое решение этой проблемы интеграции:

Вместо использования DataSourcePlatformTransactionManager в конфигурации Spring, вы будете использовать HibernateTransactionManager (при использовании обычного Hibernate) или JpaTransactionManager (при использовании Hibernate через JPA).

Специализированный HibernateTransactionManager обеспечит:

Управлять транзакциями через Hibernate, т.е. через SessionFactory.

Быть достаточно умным, чтобы позволить Spring использоватJь ту же самую транзакцию в коде Spring, не отвносящуюся к Hibernate, т.е. @Transactional.

Как всегда, картинка может быть проще для понимания (хотя обратите внимание, что поток между прокси и реальным сервисом является лишь концептуально правильным и упрощенным).

Вот, вкратце, как вы интегрируете Spring и Hibernate.

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

Заключение

К этому моменту вы должны иметь довольно хорошее представление о том, как управление транзакциями работает с фреймворком Spring и как оно применяется к другим библиотекам Spring, таким как Spring boot или Spring WebMVC. Самым главным выводом должно быть то, что в конечном итоге не имеет значения, какой фреймворк вы используете, все дело в основах JDBC.

Поймите их правильно (помните: getConnection().setAutocommit(false).commit()) и вам будет гораздо легче понять, что произойдет позже в вашем сложном корпоративном приложении.

Спасибо за прочтение.

Благодарности

Спасибо Андреасу Эйзеле за отзывы о ранних версиях этого руководства. Спасибо Бену Хорсфилду за создание очень нужных фрагментов Javascript для улучшения этого руководства.

Переведено с помощью DeepL (бесплатная версия)

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


  1. gsaw
    16.08.2022 11:59
    +7

    Зачем переводили сокращения? Эксэмэл, какая то ЯБАДАБАДУ. Почему Spring русскими буквами, а не Весна к примеру? Некоторые сокращения переведены, некоторые просто русскими буквами записанны. Просто мозг ломается при чтении. Я концентрировался на попытках понять, что это за х такое, вместо того, что бы вникнуть в тему. По сути нормально, но с руссификацией перебор.


    1. panzerfaust
      16.08.2022 12:07
      +8

      Я стойко держался аккурат до

      Если Спринг обнаруживает аннотацию @Transactionalна бобе, он создаёт динамический прокси этого боба.

      А потом у меня Исключение Переполнения Стопки кринжа произошло


      1. gsaw
        16.08.2022 12:19
        +1

        Тот случай, когда google перевод лучше


        1. Nialpe
          16.08.2022 19:02

          Там бы были любимые слова: банка(кувшин), бобы...


    1. zapisator Автор
      16.08.2022 14:04

      Спасибо, что оценили и содержание статьи тоже.

      Почему способы перевода иностранных аббревиатур разные: я интуитивно ориентировался, основываясь на рекомендации Википедии. Конкретно Спринг встечается в сети с таким переводом. Я постарался использовать правило, которое довели до меня ещё в вузе: не должно оставаться не переведённых слов. Два я всё таки оставил, встречаются всего по 1 разу, и 1 из них уж очень широко известен именн в этой форме. Н. А ещё имена классов, считая их непереводимой последовательностью символов. Очень много людей получает неверятное удовольствие оставляя слова непереведёнными. Я думал, что статья также интересна будет начинающим, для них полный перевод мне показалось будет больше всего увеличивать понимание. Прочие могут почитать и на английском.


      1. dopusteam
        16.08.2022 21:10
        +1

        не должно оставаться не переведённых слов. 

        Вы, кажется, слишком буквально восприняли эту фразу.

        Ну, если уж придираться, то почему @Transactional, например, не перевели, раз не должно оставаться непереведённых слов?

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

        И они пойдут гуглить СБДЯ и ничего не найдут


        1. zapisator Автор
          17.08.2022 10:56

          По правилам оформления, например научной статьи: первое упоминание термина идёт на русском - в скобках на родном языке, возможно с пояснениями. Все прочие встречи термина - только на русском.

          И они пойдут гуглить СБДЯ и ничего не найдут

          Так как и английское название присутствует, то в любом случае найти смогут. К тому же здесь есть отсылки и документации, и на разделы подобных статей того же автора. К тому же при первой встрече этого СБДЯ было не только дано английское название, но и перевод каждого слова из аббревиатуры.

          почему @Transactional, например, не перевели, раз не должно оставаться непереведённых слов

          Ответил на это в комментарии выше.

          А ещё имена классов, считая их непереводимой последовательностью символов.

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


        1. zapisator Автор
          17.08.2022 23:40

          Мне немного неловко просить ещё немного времени к своей работе. Можете ли сказать что-то про содержание. Надо же исправлять текущую карму новой статьёй. Ясно что нужно очень чистить и опечатки, и именно так термины записывать. Как вам содержание? Меня идея статьи зацепила тем, что очень простой пример даёт, относительно которого можно углублять любое дальнейшее понимание транзакций в Spring. Ну дальше несколько бусин, которые последовательно нанизываются на нить из этого примера. Заслуживает ли окончательный перевод такой статьи лайка?


    1. ramay
      16.08.2022 14:38
      +1

      думал у меня одного мозг от СБДЯ и всего подобного ломается


    1. zapisator Автор
      17.08.2022 11:25

      Сменил все аббревиатуры на англоязычные. Бины теперь вместо бобов.

      Надеюсь так действительно лучше. Хотя мне кажется шарм, когда не "бин", а "боб", как кофейный боб, ушёл. Всё в Java пропитано этим островом и его очень известным напитком, и столицей окаймляющего государства Jakarta.


      1. gsaw
        17.08.2022 12:14
        -1

        Да, стало на много лучше. Читается проще. Проще, когда знакомые аббревиатуры, не придуманные переводчиком. Знания же как пирамида, в основе лежат другие знания. Проще, когда нет необходимости разбирать каждую букву в тексте.


  1. aleksandy
    16.08.2022 13:01

    Переведено с помощью DeepL (бесплатная версия)

    Лучшей антирекламы сервиса не сыскать.

    А почему СБДЯ, но Спринг-МВС? Кто-нибудь вообще придумал как СБДЯ можно адекватно расшифровать?


    1. zapisator Автор
      16.08.2022 14:06

      О нет, беру ответственность за переводы аббревиатур на себя. Я всё таки правил этот текст не только по смыслу, так как ингда автоматический мне казался неверным.


  1. AstarothAst
    17.08.2022 15:45

    Да, перевести «xml», как «иксэмэль» — это заявка на включение в переводческие топы, наравне с голым кондуктором бегущим под вагонами…


    1. zapisator Автор
      17.08.2022 23:20

      Да где вы это видите, я уже кажется всё почистил.


      Как вам в целом статься-то?