Spring @Transactional(Propagation.NEVER)应该创建Hibernate会话吗?

2022-09-01 12:12:35

假设我们已经在Spring(版本4.2.7)中正确配置了由Hibernate(版本4.3.11)支持的JPA。已启用休眠第一级缓存。我们使用声明性事务。

我们有:OuterBean

@Service
public class OuterBean {

    @Resource
    private UserDao userDao;

    @Resource
    private InnerBean innerBean;

    @Transactional(propagation = Propagation.NEVER)
    public void withoutTransaction() {
        User user = userDao.load(1l);
        System.out.println(user.getName());  //return userName
        innerBean.withTransaction();
        user = userDao.load(1l);
        System.out.println(user.getName());  //return userName instead of newUserName
    }

}

这是从:InnerBeanOuterBean

@Service
public class InnerBean {

    @Resource
    private UserDao userDao;

    @Transactional
    public void withTransaction() {
        User user = userDao.load(1l);
        user.setName("newUserName");
    }

}

方法 在 中两次返回相同值(第二次是在数据库中更新名称之后)的行为是否正确?user.getName()OuterBean

换句话说,是否正确的行为为导致第二次调用从休眠第一级缓存而不是数据库读取的方法创建休眠会话?@Transactional(propagation = Propagation.NEVER)withoutTransaction()user.getName()


编辑

为了进一步解释问题,我从创建休眠会话中附加了跟踪

TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl  - Opening Hibernate Session.  tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@c17285e
TRACE org.hibernate.internal.SessionImpl  - Opened session at timestamp: 14689173439
TRACE org.hibernate.internal.SessionImpl  - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
userName
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl  - Opening Hibernate Session.  tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@715c48ca
TRACE org.hibernate.internal.SessionImpl  - Opened session at timestamp: 14689173439
TRACE org.hibernate.internal.SessionImpl  - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Automatically flushing session
TRACE org.hibernate.internal.SessionImpl  - before transaction completion
TRACE org.hibernate.internal.SessionImpl  - after transaction completion
TRACE org.hibernate.internal.SessionImpl  - Closing session
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
userName
TRACE org.hibernate.internal.SessionImpl  - Closing session

现在,让我们比较我删除时的跟踪@Transactional(propagation = Propagation.NEVER)

TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl  - Opening Hibernate Session.  tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@4ebd2c5f
TRACE org.hibernate.internal.SessionImpl  - Opened session at timestamp: 14689203905
TRACE org.hibernate.internal.SessionImpl  - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Closing session
userName
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl  - Opening Hibernate Session.  tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@5af84083
TRACE org.hibernate.internal.SessionImpl  - Opened session at timestamp: 14689203905
TRACE org.hibernate.internal.SessionImpl  - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Automatically flushing session
TRACE org.hibernate.internal.SessionImpl  - before transaction completion
TRACE org.hibernate.internal.SessionImpl  - after transaction completion
TRACE org.hibernate.internal.SessionImpl  - Closing session
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl  - Opening Hibernate Session.  tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@35f4f41f
TRACE org.hibernate.internal.SessionImpl  - Opened session at timestamp: 14689203906
TRACE org.hibernate.internal.SessionImpl  - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Closing session
newUserName

请注意,当我省略时,将为每次调用方法创建单独的会话。@Transactional(propagation = Propagation.NEVER)userDao

所以我的问题也可以表述为

不应该在Spring中作为监护人来实现,以防止我们意外使用交易,而没有任何副作用(会话创建)?@Transactional(propagation = Propagation.NEVER)


答案 1

行为是正确的 - Hibernate 将始终创建一个会话(否则您希望它如何执行任何操作?),并通过加载实体将其与该会话相关联。由于 不参与事务,因此其中所做的更改将发生在新事务中,除非您调用 ,否则不应可见,这将强制从数据库重新加载。withoutTransactionwithTransactionrefresh

我引用了Hibernate的官方文档

会话的主要功能是为映射实体类的实例提供创建、读取和删除操作。实例可能存在于以下三种状态之一:

  • 瞬态:从不持久,不与任何会话关联
  • 持久性:与分离的唯一会话相关联:以前
  • 持久性,不与任何会话关联

暂时性实例可以通过调用 或 来保持持久性。持久性实例可以通过调用 来使持久性实例成为瞬态实例。get()load() 方法返回的任何实例都是持久的。save()persist()saveOrUpdate()delete()

摘自 Java Persistence With Hibernate,第二版,作者:Christian Bauer、Gavin King 和 Gary Gregory:

持久性上下文充当第一级缓存;它会记住您在特定工作单元中处理过的所有实体实例。例如,如果您要求 Hibernate 使用主键值(按标识符查找)加载实体实例,则 Hibernate 可以首先检查持久性上下文中的当前工作单元。如果 Hibernate 在持久性上下文中找到实体实例,则不会发生数据库命中 — 这是应用程序的可重复读取。具有相同持久性上下文的连续调用将产生相同的结果。em.find(Item.class, ITEM_ID)

同样来自 Java Persistence With Hibernate, Second Edition

持久性上下文缓存始终处于打开状态,无法将其关闭。它确保以下几点:

  • 在对象图中的循环引用的情况下,持久性层不容易受到堆栈溢出的影响。
  • 在工作单元的末尾,同一数据库行永远不会有冲突的表示形式。提供程序可以安全地将对实体实例所做的所有更改写入数据库。
  • 同样,在特定持久性上下文中所做的更改对于在该工作单元及其持久性上下文中执行的所有其他代码始终立即可见。JPA 保证可重复的实体实例读取。

关于交易,以下是从Hibernate的官方文档中摘录的内容

定义用于从已配置的事务管理基础方式中抽象应用程序的协定。允许应用程序定义工作单元,同时保持从底层事务实现的抽象(例如。JTA, JDBC)。

因此,总而言之,它不会共享UnitOfWork,因此不会共享第一级缓存,这就是为什么第二次加载返回原始值的原因。withTransactionwithoutTransaction

至于这两种方法不共享工作单元的原因,可以参考Shailendra的答案。

编辑:

你似乎误解了什么。必须始终创建会话 - 这就是Hibernate的工作方式,句号。您对不创建会话的期望等于期望在没有 JDBC 连接的情况下执行 JDBC 查询:)

两个示例之间的区别在于,使用您的方法被 Spring 截获并代理,并且只为 中的查询创建了一个会话。当您删除注释时,您将方法从Spring的事务拦截器中排除,以便为每个与数据库相关的操作创建一个新会话。我再重复一遍,我再怎么强调也不过分 - 您必须有一个开放的会话来执行任何查询。@Transactional(propagation = Propagation.NEVER)withoutTransaction

就保护而言 - 尝试通过使用Propedalion.NEVER交换两种方法的注释,并使用默认注释,看看会发生什么(剧透:你会得到一个)。withTransactionwithoutTransaction@TransactionalIllegalTransactionStateException

编辑2:

至于为什么会话在外部bean中的两个负载之间共享 - 这就是应该做的,并且通过注释你的方法,你已经通知Spring它应该使用配置的事务管理器来包装你的方法。以下是官方文档对 的预期行为的描述:JpaTransactionManager@TransactionalJpaTransactionManager

PlatformTransactionManager 实现单个 JPA EntityManagerFactory。将 JPA 实体管理器从指定的工厂绑定到线程,可能允许每个工厂使用一个绑定线程的实体管理器。SharedEntityManagerCreator 和 @PersistenceContext知道绑定的实体管理器,并自动参与此类事务。支持此事务管理机制的 JPA 访问代码需要使用其中任何一个。

另外,要知道Spring是如何处理声明式事务管理的(即 方法注释),请参阅官方文档。为了便于导航,我将包括一个报价:@Transactional

关于Spring框架的声明性事务支持,需要掌握的最重要的概念是,这种支持是通过AOP代理实现的,并且事务建议是由元数据(目前基于XML或注释的)驱动的。AOP 与事务元数据的组合生成一个 AOP 代理,该代理将事务接收器与适当的 PlatformTransactionManager 实现结合使用,以围绕方法调用驱动事务


答案 2

首先,当您在JPA API后面使用休眠时,我将使用该术语而不是会话(严格相同,只是术语问题)。EntityManager

每次使用JPA访问数据库都会涉及一个,你正在获取实体,你需要一个(EM)。所谓的 1st 级缓存只不过是 EM 托管实体状态。EntityManagerEntityManager

从理论上讲,EM的生命周期很短,并且绑定到一个工作单元(因此通常绑定到事务,请参阅努力理解EntityManager的正确用法)。

现在JPA可以以不同的方式使用:容器管理或用户管理的持久性。当EM由容器管理时(您的情况,这里是弹簧是容器),最后一个负责管理EM范围/生命周期(为您创建,刷新和销毁它)。由于 EM 绑定到事务/工作单元,因此此任务被委派给 (处理注释的对象)。TransactionManager@Transactional

当您使用 对方法进行注释时,您正在创建一个弹簧逻辑事务范围,该范围将确保没有绑定到最终存在的EM的现有现有现有EM的现有现有JDBC事务,这将不会创建一个,并且将使用JDBC自动提交模式,但如果尚不存在,它将为此逻辑事务范围创建一个EM。@Transactional(propagation = Propagation.NEVER)

关于在未定义事务逻辑作用域时为每个 DAO 调用创建新的 EM 实例这一事实,您必须记住,您无法在 EM 外部使用 JPA 访问数据库。在这种情况下,AFAIK休眠用于引发错误,但这可能随着以后的版本而发展,否则您的DAO可能会被注释,如果没有封闭的逻辑作用域,它也会自动创建EM。这是一种不好的做法,因为事务应该在工作单元上定义,例如。服务级别,而不是 DAO 级别。no session bound to thread@Transactional(propagation = Propagation.SUPPORT)


推荐