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

Posted

技术标签:

【中文标题】Spring 为在 @Transactional 注释方法中调用的每个 JpaRepository 方法打开一个新事务【英文标题】:Spring opens a new transaction for each JpaRepository method that is called within an @Transactional annotated method 【发布时间】:2021-12-21 10:18:16 【问题描述】:

我有一个用@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.

这是 @Configuration 文件,启用了事务管理和 jpa 存储库

@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.

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

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

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

编辑:这里JPA - Spanning a transaction over multiple JpaRepository method calls 描述了一个类似的问题。根据用户的说法,spring仅在存储库实现Repository而不是CrudRepositoryJpaRepository时使用现有事务。但这是一种解决方法。

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

【问题讨论】:

不是说它会有所作为,但是您是否检查过您是否在所有地方都导入了相同的@Transactional(有两个:JPA 和 Spring 之一)? @WimDeblauwe 我刚刚检查过。它使用org.springframework.transaction.annotation.Transactional 之一。 我怀疑一切正常,而您只是被启用了 TRACE 日志记录的事实吓倒了。 @M.Deinum 你到底是什么意思?我故意通过将logging.level.org.springframework.transaction.interceptor 设置为TRACE 来打开TRACE,这样我就可以检查事务是否是由带注释的方法创建的。事实证明是这种情况,但是根据日志,de JpaRepository 方法正在执行相同的操作。他们需要使用调用方法打开的事务,而不是自己启动。 对于完整的图片,您应该为org.springframework.transaction 启用调试/跟踪日志记录。您现在只查看日志的单个部分。此外,如果您希望使用 customTransactionManager,则需要在 @Transactional 注释中指定,否则它将使用默认值。另一个提示沟@Repository 在您的界面上,它没有任何作用,我也会删除 hte @Transactional (也不会添加任何东西)。 【参考方案1】:

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

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”的行——这意味着事务是在方法调用开始时创建的。

还要注意“Participating in existing transaction”语句,它表示该方法正在参与事务。

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> 

【讨论】:

感谢@Shailendra 的回答。在包含JpaTRansactionManager=DEBUG 之后,我也看到了日志participating in existing transaction。但我也看到它仍然为findAll 打开一个新交易。您可以在日志摘录中看到相同的内容。只需看看从顶部数起的第九行。它以2021-11-09 11:43:06.333 TRACE 24956 --- 开头。它清楚地表明它正在为 findAll 打开一个新交易,尽管它在此之前说Participating in existing transaction。为什么这样做?这些是嵌套事务还是什么? 我现在明白发生了什么。在做了更多研究并阅读了有关事务docs.spring.io/spring-framework/docs/current/reference/html/… 的spring 文档之后,我现在明白为findAll 打开的新事务实际上是logical transaction 而不是physical。所以 jpa 方法确实使用了调用方法创建的物理事务。【参考方案2】:

在尝试了不同的方法后,包括使用TransactionTemplate,我决定采用以下解决方案:

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

@EnableJpaRepositories(enableDefaultTransactions = false)

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

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

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

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

@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 方法调用创建一个新事务时,在性能方面是非常昂贵的。所以这就是我现在要解决的解决方案。

要测试您自己的交易,您需要将其添加到您的 application.properties

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

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

编辑:

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

这个SO线程的最后一个答案也很好地解释了逻辑事务和物理事务的区别:Difference between physical and logical transactions in spring

【讨论】:

以上是关于Spring 为在 @Transactional 注释方法中调用的每个 JpaRepository 方法打开一个新事务的主要内容,如果未能解决你的问题,请参考以下文章

Spring - @Transactional - 在后台发生了啥?

Spring @Transactional 和 Spring @Lock 注解有啥关系?

Spring @Transactional 使用

Spring @Transactional 只读传播

Spring @Transactional 属性是不是适用于私有方法?

如何在 Spring Boot 中使用 @Transactional 注解