Статьи

Объясненные пружинные транзакции — часть 2 (JPA)

В первой части серии я показал, как транзакции работают в обычном JDBC . А затем я показал, как Spring управляет транзакциями на основе JDBC. Во второй части этой серии я покажу, как транзакции сначала работают в обычном JPA . А затем покажите, как Spring управляет JPA-транзакциями.

Перевод средств

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
... class BankAccountService {
  public void transfer(MonetaryAmount amount, ...) {
    debit(amount, ...);
    credit(amount, ...);
    ...
  }
  public void credit(MonetaryAmount amount, AccountId accountId) {
    ...
  }
  public void debit(MonetaryAmount amount, AccountId accountId) {
    ...
  }
  ...
}

JPA Сделки

В обычном JPA транзакции начинаются с вызова getTransaction().begin() в EntityManager . Фрагмент кода ниже иллюстрирует это.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
import javax.persistence.*;
...
EntityManagerFactory emf = ...;
EntityManager em = emf.createEntityManager();
try {
  em.getTransaction().begin();
  // make changes through entities
  em.getTransaction().commit();
  ...
} catch(Exception e) {
  em.getTransaction().rollback();
  throw e;
} finally {
  em.close();
}

Технически EntityManager находится в транзакции с момента его создания. Поэтому вызов begin() несколько избыточен. До begin() некоторые операции, такие как persist , merge , remove не могут быть вызваны. Запросы все еще могут быть выполнены (например, find() ).

Объекты, которые были возвращены из запросов, могут быть изменены. Хотя в спецификации JPA неясно, что произойдет с этими изменениями, когда транзакция не была начата.

Теперь давайте применим JPA к конкретному примеру перевода средств.

Мы определили сущность BankAccount для обработки поведения debit() и credit() .

01
02
03
04
05
06
07
08
09
10
import javax.persistence.*;
 
@Entity
... class BankAccount {
  @Id ...;
  ...
  public void debit(MonetaryAmount amount) {...}
  public void credit(MonetaryAmount amount) {...}
  ...
}

Мы добавляем EntityManagerFactory в BankAccountService чтобы при необходимости BankAccountService было создавать BankAccountService EntityManager .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import javax.persistence.*;
 
... class BankAccountService {
  private EntityManagerFactory emf; // injected via constructor
  ...
  public void transfer(MonetaryAmount amount, ...) ... {
    EntityManager em = emf.createEntityManager();
    try {
      em.getTransaction().begin();
      BankAccount fromAccount = em.find(BankAccount.class, ...);
      BankAccount toAccount = em.find(BankAccount.class, ...);
      fromAccount.debit(amount);
      toAccount.credit(amount);
      em.getTransaction().commit();
      ...
    } catch(Exception e) {
      em.getTransaction().rollback();
      // handle exception (possibly rethrowing it)
    } finally {
      em.close();
    }
  }
  public void credit(MonetaryAmount amount, AccountId ...) ... {
    EntityManager em = emf.createEntityManager();
    try {
      em.getTransaction().begin();
      BankAccount theAccount = em.find(BankAccount.class, ...);
      theAccount.credit(amount);
      em.getTransaction().commit();
      ...
    } catch(Exception e) {
      em.getTransaction().rollback();
      // handle exception (possibly rethrowing it)
    } finally {
      em.close();
    }
  }
  public void debit(MonetaryAmount amount, AccountId ...) ... {
    EntityManager em = emf.createEntityManager();
    try {
      em.getTransaction().begin();
      BankAccount theAccount = em.find(BankAccount.class, ...);
      theAccount.debit(amount);
      em.getTransaction().commit();
      ...
    } catch(Exception e) {
      em.getTransaction().rollback();
      // handle exception (possibly rethrowing it)
    } finally {
      em.close();
    }
  }
}

Сделки с пружиной под управлением JPA

Методы JdbcTemplate и JdbcTemplate могут использовать класс шаблона (что-то вроде JdbcTemplate ) для удаления всего стандартного кода. Ранее Spring предоставлял класс JpaTemplate , но с весны 3.1 он был устаревшим, в пользу использования нативного EntityManager (обычно получаемого через @PersistenceContext ).

Итак, давайте сделаем это — используйте EntityManager полученный через @PersistenceContext .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import javax.persistence.*;
 
... class BankAccountService {
  @PersistenceContext
  private EntityManager em;
  ...
  public void transfer(MonetaryAmount amount, ...) ... {
    try {
      em.getTransaction().begin();
      BankAccount fromAccount = em.find(BankAccount.class, ...);
      BankAccount toAccount = em.find(BankAccount.class, ...);
      fromAccount.debit(amount);
      toAccount.credit(amount);
      em.getTransaction().commit();
      ...
    } catch(Exception e) {
      em.getTransaction().rollback();
      // handle exception (possibly rethrowing it)
    }
  }
  public void credit(MonetaryAmount amount, AccountId ...) ... {
    try {
      em.getTransaction().begin();
      BankAccount theAccount = em.find(BankAccount.class, ...);
      theAccount.credit(amount);
      em.getTransaction().commit();
      ...
    } catch(Exception e) {
      em.getTransaction().rollback();
      // handle exception (possibly rethrowing it)
    }
  }
  public void debit(MonetaryAmount amount, AccountId ...) ... {
    try {
      em.getTransaction().begin();
      BankAccount theAccount = em.find(BankAccount.class, ...);
      theAccount.debit(amount);
      em.getTransaction().commit();
      ...
    } catch(Exception e) {
      em.getTransaction().rollback();
      // handle exception (possibly rethrowing it)
    }
  }
}

Наш код немного проще. Поскольку мы не создали EntityManager , нам не нужно его закрывать. Но мы все еще вызываем getTransaction().begin() . Есть ли способ лучше? И как EntityManager вводится в объект в первую очередь?

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

EntityManager и @PersistenceContext

Мы говорим Spring, чтобы внедрить EntityManager из EntityManagerFactory , добавив PersistenceAnnotationBeanPostProcessor (либо через XML <bean> , либо просто используя конфигурацию на основе Java через классы @Configuration загруженные через AnnotationConfigApplicationContext ).

  • При использовании конфигурации на основе XML элемент PersistenceAnnotationBeanPostProcessor прозрачно активируется элементом <context:annotation-config /> . И этот элемент также прозрачно активируется с помощью <context:component-scan /> .
  • При использовании @Configuration основе @Configuration используется AnnotationConfigApplicationContext . При этом процессоры конфигурации аннотаций всегда регистрируются (одним из которых является вышеупомянутый PersistenceAnnotationBeanPostProcessor ).

При добавлении одного определения бина контейнер Spring будет действовать как контейнер JPA и EnitityManager из вашего EntityManagerFactory .

JPA и @Transactional

Теперь, когда у нас есть EntityManager , как мы можем сказать Spring, чтобы мы начинали транзакции?

Мы говорим Spring запускать транзакции, помечая методы как @Transactional (или @Transactional класс как @Transactional что делает все открытые методы транзакционными). Это согласуется с тем, как Spring разрешает транзакции с JDBC.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import javax.persistence.*;
import org.springframework.transaction.annotation.Transactional;
 
@Transactional
... class BankAccountService {
  @PersistenceContext
  private EntityManager em;
  ...
  public void transfer(MonetaryAmount amount, ...) ... {
      BankAccount fromAccount = em.find(BankAccount.class, ...);
      BankAccount toAccount = em.find(BankAccount.class, ...);
      fromAccount.debit(amount);
      toAccount.credit(amount);
  }
  public void credit(MonetaryAmount amount, AccountId ...) ... {
      BankAccount theAccount = em.find(BankAccount.class, ...);
      theAccount.credit(amount);
  }
  public void debit(MonetaryAmount amount, AccountId ...) ... {
      BankAccount theAccount = em.find(BankAccount.class, ...);
      theAccount.debit(amount);
  }
}

Вау, это было мило! Наш код стал намного короче.

И так же, как объяснялось в первой части этой серии , когда Spring встречает эту аннотацию, он проксирует объект (обычно называемый bean-компонентом, управляемым Spring). Прокси-сервер запускает транзакцию (если нет @Transactional транзакции) для методов, помеченных как @Transactional , и завершает транзакцию, когда метод успешно завершается.

1f997647

Вызов debit() будет использовать транзакцию. Отдельный вызов credit() будет использовать транзакцию. Но что происходит при вызове метода transfer() ?

Поскольку метод @Transactional transfer() помечен как @Transactional , Spring начнет транзакцию. Эта же транзакция будет использоваться для вызовов debit() и credit() . Другими словами, debit(amount) и credit(amount) не начнут новую транзакцию. Он будет использовать текущую транзакцию (поскольку она есть).

Но ждать! Как Spring узнает, когда ввести правильного менеджера сущностей? Он вводится только при вызове транзакционного метода?

Shared EntityManager

В одном из моих учебных занятий я попробовал следующее, чтобы лучше понять, как Spring внедряет EntityManager через @PersistenceContext . И я верю, что это поможет и другим. Итак, вот что я попробовал:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
import javax.persistence.*;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.InitializingBean;
 
@Transactional
... class BankAccountService implements InitializingBean {
  @PersistenceContext
  private EntityManager em;
  ...
  @Override
  public void afterPropertiesSet() {
    System.out.println(em.toString());
  }
  ...
}

Вывод чего-то подобного отображался на консоли после запуска контекста приложения.

1
Shared EntityManager proxy for target factory [...]

Итак, что же это за менеджер общих сущностей?

Когда запускается контекст приложения, Spring внедряет менеджер общих сущностей. Общий EntityManager будет вести себя так же, как EntityManager извлекаемый из среды JNDI сервера приложений, как определено в спецификации JPA. Он будет делегировать все вызовы текущему транзакционному EntityManager , если таковые имеются; в противном случае он будет использовать только что созданный EntityManager каждой операции.

Возвращаясь к нашему вопросу. Spring не вводит правильного менеджера сущности в нужное время. Он всегда внедряет менеджера общей сущности. Но этот менеджер общих сущностей осведомлен о транзакциях. Он делегирует текущему транзакционному EntityManager , если есть текущая транзакция.

Вывод

ириски-пиво На этом заканчивается серия из двух частей. Я надеюсь, что, начав с простых версий JDBC и JPA (без DAO и репозиториев), я смог прояснить, как Spring может управлять транзакциями за кулисами. И что, имея более четкое представление о том, что Spring делает за кулисами, вы можете лучше устранять неполадки, понимать, почему вы получаете TransactionRequiredException говоря «Нет доступного транзакционного EntityManager», и добавлять более качественные исправления в свои приложения.

Теперь пришло время для холодной.