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

Транзакция — это очень важное понятие в мире баз данных (и не только). Каждый, кто работал с базами данных, слышал про ACID и транзакции. Если кто не работал и не знает что это такое, то всё просто: транзакция это набор операций, которые могут быть либо целиком и успешно выполнены, либо полностью не выполнены.

Управление транзакциями поддерживается и в JDBC, и в JPA, и в Hibernate. И везде управление реализовано примерно одинаково:

  1. Открываем транзакцию
  2. Проводим какие-то действия над данными
  3. Подтверждаем, либо откатываем транзакцию

Например, код сохранения объекта в JPA может выглядеть так:

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(g);
em.getTransaction().commit();

На четыре строки кода только одна решает проблему разработчика, а все остальные нужны только для управления persistence context и транзакциями. Конечно 3/4 кода отдавать на служебные нужды это с одной стороны неприятно, с другой приемлемо. Но эти три четверти приходится повторять постоянно. Каждый метод, обращающийся к JPA, будет открывать persistence context, начинать транзакцию и завершать её. Сплошное самоповторение и скукота.

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

Подготовка

Нам понадобится пустой maven проект с Spring, Spring ORM, H2, Hibernate в качестве реализации JPA и библиотеками тестирования:

<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <javaee.version>7.0</javaee.version>
  <lombok.version>1.16.12</lombok.version>
  <org.springframework.version>4.3.4.RELEASE</org.springframework.version>
  <hibernate.version>5.2.5.Final</hibernate.version>
  <h2.version>1.4.190</h2.version>
  <junit.version>4.12</junit.version>
  <hamcrest.version>1.3</hamcrest.version>
  <easymock.version>3.3.1</easymock.version>
</properties>


<dependencies>
  <dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>7.0</version>
  </dependency>


  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.version}</version>
  </dependency>


  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${org.springframework.version}</version>
  </dependency>


  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>${org.springframework.version}</version>
  </dependency>


  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>${org.springframework.version}</version>
  </dependency>


  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>${org.springframework.version}</version>
    <scope>test</scope>
  </dependency>


  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>${h2.version}</version>
  </dependency>


  <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>${hibernate.version}</version>
  </dependency>


  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>${junit.version}</version>
    <scope>test</scope>
  </dependency>


  <dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-library</artifactId>
    <version>${hamcrest.version}</version>
    <scope>test</scope>
  </dependency>


  <dependency>
    <groupId>org.easymock</groupId>
    <artifactId>easymock</artifactId>
    <version>${easymock.version}</version>
  </dependency>
</dependencies>

Настройка  JPA

Конфигурация JPA располагается в файле META-INF/persistence.xml В моём примере я использую Hibernate в качестве реализации JPA и встраиваемую базу H2 в качестве базы данных.

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
  <persistence-unit name="ru.easyjava.spring.data.jpa">
    <properties>
      <property name="hibernate.hbm2ddl.auto" value="update"/>
      <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
      <property name="hibernate.connection.url" value="jdbc:h2:mem:test"/>
    </properties>
  </persistence-unit>
</persistence>

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

Настройка Spring и управления транзакциями

Для разнообразия настроим контекст Spring используя Java  конфигурацию. Это поможет продемонстрировать, как устроено управление транзакциями в Spring. Spring определяет интерфейс PlatformTransactionManager, для которого существуют разные реализации для использования с разными ресурсами.  Например, для JPA  используется JpaTransactionManager, который конфигурируется EntityManagerFactory:

@Configuration
@EnableTransactionManagement
public class DaoConfiguration {
  @Bean
  public LocalEntityManagerFactoryBean emf() {
    LocalEntityManagerFactoryBean result = new LocalEntityManagerFactoryBean();
    result.setPersistenceUnitName("ru.easyjava.spring.data.transactions");
    return result;
  }


  @Bean
  public PlatformTransactionManager transactionManager() {
    JpaTransactionManager result = new JpaTransactionManager();
    result.setEntityManagerFactory(emf().getObject());
    return result;
  }
}

Аннотация @EnableTransactionManagenent включает поддержку декларативного управления транзакциями, а создание JpaTransactionManager  подкладывает конкретную реализацию под управление транзакциями.

Аналогично можно создать менеджер транзакций для JDBC или Hibernate. Для JDBC следует использовать DataSourceTransactionManager, для Hibernate — HibernateTransactionManager.

 Уровень DAO

Схема данных абсолютно идентичная схеме, используемой в примере Spring ORM и JPA. А вот реализация уровня данных заметно отличается:

public interface GreeterDao {
  void addGreet(Greeter g);
  void updateGreet(Greeter g, String newTarget);
  List<Greeter> getGreetings();
}
@Repository
public class GreeterDaoImpl implements GreeterDao {
  @PersistenceContext
  private EntityManager em;


  @Override
  @Transactional
  public final void addGreet(final Greeter g) {
    em.persist(g);
  }


  @Override
  @Transactional(rollbackFor = NotImplementedException.class)
  public final void updateGreet(final Greeter g, final String newTarget) {
    Greeter greet = em.merge(g);
    greet.setTarget(newTarget);


    throw new NotImplementedException();
  }


  @Override
  @Transactional(readOnly = true)
  public final List<Greeter> getGreetings() {
    return em.createQuery("from Greeter", Greeter.class)
          .getResultList();
  }
}
@ContextConfiguration(classes = ru.easyjava.spring.data.declarative.ContextConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class GreeterDaoImplIT {
  @Inject
  private GreeterDao testedObject;


  @DirtiesContext
  @Test
  public void testRetrieve() {
    Greeter expected = new Greeter();
    expected.setGreeting("TEST");
    expected.setTarget("TEST");


    testedObject.addGreet(expected);


    List<Greeter> actual = testedObject.getGreetings();
    Iterator<Greeter> it = actual.iterator();
    Greeter actualGreet =it.next();
    assertThat(actualGreet.getGreeting(), is("TEST"));
    assertThat(actualGreet.getTarget(), is("TEST"));
  }
}

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

Аннотация @Transactional говорит Spring, что перед вызовом метода надо породить новый (не всегда новый, но об этом в следующей статье) persistence context (или запросить новое JDBC соединение) и начать в нём транзакцию. А после того как метод завершится, транзакцию необходимо подтвердить.

Кстати, если аннотацию @Transactional пропустить, то вызов такого метода вернёт ошибку:

javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread

Кроме того, у использования @Transactional есть неочевидное ограничение. Так как управление транзакциями реализовано путём создания прокси объектов времени исполнения, то при вызове метода с @Transactional напрямую, а не через прокси, то вызов провалится с такой же ошибкой. Если говорить проще, то вызывая метод у Spring bean вы в безопасности, во всех остальных случаях — нет:

class SomeDao {
  @PersistenceContext
  EntityManager em;


  @Transactional
  public void query() {
    em.persist();
  }


  public void anotherQuery() {
    this.query(); //Вызов мимо прокси
  }
}


@Service
class SomeService {
  @Inject
  SomeDao dao;


  public SomeMethod() {
    dao.query; //Это работает
  }


  public OtherMethod() {
    dao.anotherQuery(); //А это провалится
  }
}

@Transactional

Аннотация @Transactional поддерживает несколько параметров, которые задают поведение транзакции:

  • value и transactionManager — указывают, какой именно экземпляр PlatformTransactionManager использовать, если их несколько.
  • readOnly — указывает, что транзакция только читает данные, но не изменяет их. Это может быть использовано для оптимизации запросов или блокировок на уровне базы.
  • timeout — задаёт максимальную длительность операции на стороне базы данных и если эта длительность будет превышена, метод прервётся и транзакция откатится
  • rollbackFor/ rollbackForClassName — задают список классов исключений, которые вызовут откат транзакции, если метод их выбросит. В коде выше метод updateGreet() именно так откатывает транзакцию. По умолчанию, каждое исключение, имеющее в предках RuntimeError, вызывает откат транзакции.
  • noRollbackFor/ noRollbackForClassName — задают список классов исключений, которые не вызовут откат транзакции, если метод их выбросит.
  • propagation и isolation — управляют распространением транзакции и её уровнем изоляции. Я опишу эти параметры в отдельной статье.

В кратце метод с @Transactional можно рассматривать так: при входе в метод автоматически создаётся транзакция и открывается соединение с базой данных или создаётся persistence context. При выходе из функции транзакция автоматически подтверждается. Если функция кидает RuntimeError или его наследника (или настроенное исключение), транзакция автоматически откатывается. В любом случае, после выхода из метода соединение с базой закрывается, а persistence context разрушается.

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

Напишем сервис, который будет использовать транзакционное DAO и как нибудь его поиспользуем:

public interface GreeterService {
    String greet();
}
@Service
public class GreeterServiceImpl implements GreeterService {
    @Inject
    private GreeterDao dao;


    @Override
    public final String greet() {
        List<Greeter>  greets = dao.getGreetings();
        Iterator<Greeter> it = greets.iterator();
        if (!it.hasNext()) {
            return "No greets";
        }
        Greeter greeter = it.next();
        return greeter.getGreeting() + ", " + greeter.getTarget();
    }
}
public class GreeterServiceImplTest extends EasyMockSupport {
    @Rule
    public EasyMockRule em = new EasyMockRule(this);


    @Mock
    private GreeterDao dao;


    @TestSubject
    private GreeterServiceImpl testedObject = new GreeterServiceImpl();


    @Test
    public void testNoGreets() {
        expect(dao.getGreetings()).andReturn(Collections.EMPTY_LIST);


        replayAll();


        assertThat(testedObject.greet(), is("No greets"));
    }


    @Test
    public void testGreets() {
        Greeter expected = new Greeter();
        expected.setGreeting("TEST");
        expected.setTarget("TEST");
        expect(dao.getGreetings()).andReturn(Collections.singletonList(expected));


        replayAll();


        assertThat(testedObject.greet(), is("TEST, TEST"));
    }
}

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

public static void main(final String[] args) {
  ApplicationContext context =
          new AnnotationConfigApplicationContext(ContextConfiguration.class);
  GreeterService greeterService = context.getBean(GreeterService.class);
  GreeterDao dao = context.getBean(GreeterDao.class);


  Greeter greeter = new Greeter();
  greeter.setGreeting("Hello");
  greeter.setTarget("World");


  dao.addGreet(greeter);


  System.out.println(greeterService.greet());


  try {
    dao.updateGreet(greeter, "Fail");
  } catch (NotImplementedException e) {
    // Do nothing
  }


  System.out.println(greeterService.greet());


  System.exit(0);
}
Dec 26, 2016 2:23:12 PM org.springframework.orm.jpa.LocalEntityManagerFactoryBean buildNativeEntityManagerFactory
INFO: Initialized JPA EntityManagerFactory for persistence unit 'ru.easyjava.spring.data.transactions'
Dec 26, 2016 2:23:12 PM org.hibernate.hql.internal.QueryTranslatorFactoryInitiator initiateService
INFO: HHH000397: Using ASTQueryTranslatorFactory
Hello, World
Hello, World

Обратите внимание, что транзакция по смене данных откатилась и фактического изменения не произошло, как мы и ожидали.

Скачать код примера

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *