Spring 为每个在@Transactional注释方法中调用的 JpaRepository 方法打开一个新事务



我有一个用@Transactional注释的方法。这应该意味着在此方法中触发的任何数据库查询都应使用相同的事务。但实际上,这并没有发生。确实发生的情况是为方法本身打开了一个事务,但是当调用第一个JpaRepository方法时,会为该特定方法调用打开一个新事务。

更复杂的是,对于自定义存储库方法,仅当JpaRepositoryJpaRepository custom method也用@Transactional注释时,才会打开此新事务。 如果没有,我得到以下跟踪日志语句:

无需创建事务 [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findFirstByIdNotNull]: 此方法不是事务性的。

因此,它不会创建新事务,但似乎也不会使用调用方法创建的事务。

下面是存储库类:

@Repository
public interface LanguageDao extends JpaRepository<Language, Long> {
@Transactional
public Language findByLanguageCode(String languageCode);
public Language findByIdNotNull();
}

下面是使用不同存储库方法的方法。

@Transactional
public void afterSingletonsInstantiated() {
languageDao.findByLanguageCode(); //This custom method opens a new transaction, but only because i've annotated this method with @Transactional as well.
languageDao.findAll(); //This one as well because its a standard JpaRepository method.
languageDao.findByIdNotNull();//This custom method doesn't because it lacks its own @Transactional annotation.
}

下面是启用了事务管理和 jpa 存储库的@Configuration文件。

@EnableJpaRepositories(basePackages={"DAOs"}, transactionManagerRef = "customTransactionManager", enableDefaultTransactions = true)
@EnableTransactionManagement
@Configuration
public class RootConfig implements InitializingBean {
@Bean(name = "customTransactionManager")
JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
if (shouldCreateInitialLuceneIndex) { 
EntityManager entityManager = entityManagerFactory.createEntityManager();
createInitialLuceneIndex(entityManager);
entityManager.close();
}
return transactionManager;
}
}

相关application.properties设置

spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.database-platform = org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.open-in-view = false

一些实际日志。第一行显示已创建方法afterSingletonsInstantiated的事务。

[TRACE] 2021-11-08 15:32:40.811 [main] TransactionInterceptor - Getting transaction for [config.StartupChecks$$EnhancerBySpringCGLIB$$134b7631.afterSingletonsInstantiated]
[INFO ] 2021-11-08 15:32:40.815 [main] StartupChecks - Calling sequence table reset procedure
[DEBUG] 2021-11-08 15:32:40.833 [main] SQL - {call RESET_SEQUENCE_TABLE_VALUES_TO_LATEST_ID_VALUES()}
[INFO ] 2021-11-08 15:32:41.087 [main] StartupChecks - Sequence tables reset call finished!
[INFO ] 2021-11-08 15:32:41.087 [main] StartupChecks - doing stuff
[INFO ] 2021-11-08 15:32:41.087 [main] StartupChecks - testing!
[TRACE] 2021-11-08 15:32:41.087 [main] TransactionInterceptor - Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll]
[DEBUG] 2021-11-08 15:32:41.088 [main] SQL - select language0_.id as id1_77_, language0_.dateCreated as datecrea2_77_, language0_.englishLanguageName as englishl3_77_, language0_.languageCode as language4_77_, language0_.rightToLeft as righttol5_77_, language0_.translatedLanguageName as translat6_77_ from languages language0_
[TRACE] 2021-11-08 15:32:41.091 [main] TransactionInterceptor - Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll]
[INFO ] 2021-11-08 15:32:41.091 [main] StartupChecks - end test!
[TRACE] 2021-11-08 15:32:41.091 [main] TransactionInterceptor - Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findByLanguageCode]
[DEBUG] 2021-11-08 15:32:41.112 [main] SQL - select language0_.id as id1_77_, language0_.dateCreated as datecrea2_77_, language0_.englishLanguageName as englishl3_77_, language0_.languageCode as language4_77_, language0_.rightToLeft as righttol5_77_, language0_.translatedLanguageName as translat6_77_ from languages language0_ where language0_.languageCode=?
[TRACE] 2021-11-08 15:32:41.113 [main] TransactionInterceptor - Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findByLanguageCode]
[TRACE] 2021-11-08 15:32:41.113 [main] TransactionInterceptor - No need to create transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findFirstByIdNotNull]: This method is not transactional.
[DEBUG] 2021-11-08 15:32:41.115 [main] SQL - select authority0_.ID as id1_7_, authority0_.dateCreated as datecrea2_7_, authority0_.NAME as name3_7_ from AUTHORITY authority0_ where authority0_.ID is not null limit ?
[TRACE] 2021-11-08 15:32:41.120 [main] TransactionInterceptor - No need to create transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findFirstByIdNotNull]: This method is not transactional.

以下是我已经尝试过的事情的列表。

  1. 用@Transactional(传播=传播)或languageDao@Transactional(propagation = Propagation.NESTED)。 休眠不支持NESTED,因此这会导致错误,即使我将嵌套事务允许设置为在事务管理器上true,此错误仍然存在。设置SUPPORTS将被忽略。存储库仍会为调用的每个方法启动一个新事务。(更新:Propagation.MANDATORY也没有效果)
  2. 我将我的事务管理器命名为customTransactionManager,并将其作为参数添加到@EnableJpaRepositories中,如下所示:@EnableJpaRepositories(basePackages={"DAOs"}, transactionManagerRef = "customTransactionManager")
  3. 我已将@EnableJpaRepositoriesenableDefaultTransactions设置为false。这会导致默认方法(如findAll()save())默认不再在事务中执行。但是,它不会强制他们使用用@Transactional注释的调用方法的事务。

所以我的问题是:如何使(自定义)jpa 存储库使用由调用方法启动的事务?

编辑:这里JPA - 跨越多个JpaRepository方法调用的事务描述了类似的问题。根据用户的说法,spring只在仓库实现Repository时才使用现有的事务,而不是CrudRepositoryJpaRepository。但这是一种解决方法。

编辑2:当我删除@EnableTransactionManagement时,我的@Transactional注释继续工作。根据这篇文章,当我使用spring-boot-starter-jdbcspring-boot-starter-data-jpa as a dependency时可能会发生,我这样做了。这些依赖项是否会以某种方式干扰事务管理器的正常工作?

这是我试图理解你的问题的尝试。我建议启用额外的调试

logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG

我的测试服务类 - 请注意,这被标记为事务性 - 目前这是放置它的唯一位置,因为这是我们想要的 - 创建一个事务边界。

@Service
public class LanguageService {
@Autowired
private LanguageRepository languageRepository;
@Transactional
public void runAllMethods() {
languageRepository.findByLanguageCode("en");
languageRepository.findAll();
languageRepository.findByIdNotNull();
}
}

接下来是存储库 - 没有事务注释。

public interface LanguageRepository extends JpaRepository<Language, Long> {
public Language findByLanguageCode(String languageCode);
public Language findByIdNotNull();
}

现在通过控制器访问服务 - 我在日志下方。请注意"创建名为 [com.shailendra.transaction_demo.service.LanguageService.runAllMethods]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT 的新事务"的行 - 这意味着事务是在方法调用开始时创建的。

另请注意语句"参与现有事务",该语句指示该方法正在参与事务。

2021-11-09 11:43:06.061 DEBUG 24956 --- [nio-8181-exec-1] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(2084817241<open>)] for JPA transaction
2021-11-09 11:43:06.061 DEBUG 24956 --- [nio-8181-exec-1] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [com.shailendra.transaction_demo.service.LanguageService.runAllMethods]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2021-11-09 11:43:06.069 DEBUG 24956 --- [nio-8181-exec-1] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@3107a702]
2021-11-09 11:43:06.069 TRACE 24956 --- [nio-8181-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.shailendra.transaction_demo.service.LanguageService.runAllMethods]
2021-11-09 11:43:06.099 TRACE 24956 --- [nio-8181-exec-1] o.s.t.i.TransactionInterceptor           : No need to create transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findByLanguageCode]: This method is not transactional.
Hibernate: select language0_.id as id1_0_, language0_.date_created as date_cre2_0_, language0_.english_language_name as english_3_0_, language0_.language_code as language4_0_, language0_.right_to_left as right_to5_0_, language0_.translated_language_name as translat6_0_ from language language0_ where language0_.language_code=?
2021-11-09 11:43:06.333 DEBUG 24956 --- [nio-8181-exec-1] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(2084817241<open>)] for JPA transaction
2021-11-09 11:43:06.333 DEBUG 24956 --- [nio-8181-exec-1] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
2021-11-09 11:43:06.333 TRACE 24956 --- [nio-8181-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll]
Hibernate: select language0_.id as id1_0_, language0_.date_created as date_cre2_0_, language0_.english_language_name as english_3_0_, language0_.language_code as language4_0_, language0_.right_to_left as right_to5_0_, language0_.translated_language_name as translat6_0_ from language language0_
2021-11-09 11:43:06.348 TRACE 24956 --- [nio-8181-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll]
2021-11-09 11:43:06.348 TRACE 24956 --- [nio-8181-exec-1] o.s.t.i.TransactionInterceptor           : No need to create transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findByIdNotNull]: This method is not transactional.
Hibernate: select language0_.id as id1_0_, language0_.date_created as date_cre2_0_, language0_.english_language_name as english_3_0_, language0_.language_code as language4_0_, language0_.right_to_left as right_to5_0_, language0_.translated_language_name as translat6_0_ from language language0_ where language0_.id is not null
2021-11-09 11:43:06.348 TRACE 24956 --- [nio-8181-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.shailendra.transaction_demo.service.LanguageService.runAllMethods]
2021-11-09 11:43:06.348 DEBUG 24956 --- [nio-8181-exec-1] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
2021-11-09 11:43:06.348 DEBUG 24956 --- [nio-8181-exec-1] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(2084817241<open>)]

对于只读方法(如 findAll),您会看到"无需创建事务" - 这是因为尽管默认的存储库实现"SimpleJpaRepository"被标记为事务性,但只读方法并未标记为事务性。

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

在具有多个数据源时遇到相同的问题,因此具有多个事务管理器。显然,问题在于标记为@Transactional的服务方法使用主事务管理器,而存储库配置为使用自定义事务管理器:

@EnableJpaRepositories(
basePackageClasses = {
MyRepository.class
},
entityManagerFactoryRef = "customEntityManager",
transactionManagerRef = "customTransactionManager"
)

使用 spring 对服务方法的注释解决了该问题transactionManager并指定了参数@Transactional(transactionManager = "customTransactionManager")

在尝试了不同的事情(包括使用TransactionTemplate)之后,我选择了以下解决方案:

首先,我关闭了 jparepository 方法的默认事务策略,方法是使用以下configuration类进行注释:

@EnableJpaRepositories(enableDefaultTransactions = false)

enableDefaultTransactions = false会导致任何继承的 JpaRepository 方法在调用时停止创建事务。只有用@Transactional显式注释的 jpa 方法才会在调用时继续创建新事务。

所有其他事务现在将使用由调用方法启动的任何事务,例如用@Transactional注释的服务方法。

但这并不明显,因为This method is not transactional日志跟踪消息仍将为任何未使用 @Transactional 显式注释的 jpa 方法生成。这可能有点令人困惑。

但是,我已经证明这些方法确实使用以下自定义更新方法测试调用方法的事务。

@Modifying
@Query("UPDATE User u SET u.userStatus = 1 WHERE u.userStatus = 0")
public void resetActiveUserAccountsToStatusOffline();  

这样的方法需要有一个事务,否则会引发异常javax.persistence.TransactionRequiredException: Executing an update/delete query。但正如你所看到的,这个jpa方法没有用@Transactional注释,所以它确实使用了由调用服务方法启动的事务。

设置enableDefaultTransactions = false有一个小缺点,那就是继承方法(如findAll)的事务类型并不总是使用只读事务。这实际上取决于服务级别事务是否为只读。但是,您仍然可以重写 findAll 方法并使用Transactional(readOnly = false)显式注释它。另一件需要注意的事情是,任何调用方法都必须始终用@Transactional注释,否则 jpa 方法将在事务之外运行。

我认为优点远远超过这些小缺点。因为为每个 jpa 方法调用创建新事务时,性能成本非常高。所以这就是我现在要解决的解决方案。

要测试您自己的事务,您需要将其添加到您的应用程序中。

logging.level.org.springframework.transaction.interceptor=TRACE

如果该设置不起作用,请Log4j2添加到您的项目中。

编辑:

JpaMethods 打开的这些附加事务仅在调用方法已创建physical transaction时才logical transactions。更多关于这里的信息:https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction 这些 jpa 方法仍然使用调用方法创建的事务。

这个SO线程中的最后一个答案也很好地解释了逻辑事务和物理事务之间的区别:春季物理事务和逻辑事务的区别

最新更新