【问题标题】:Spring @Transactional(Propagation.NEVER) should create Hibernate session?Spring @Transactional(Propagation.NEVER) 应该创建 Hibernate 会话吗?
【发布时间】:2016-11-16 00:15:09
【问题描述】:

假设我们已经在 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
    }

}

还有从OuterBean调用的InnerBean

@Service
public class InnerBean {

    @Resource
    private UserDao userDao;

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

}

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

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


编辑

为了解释更多问题,我附上了创建休眠会话的跟踪

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 的每次方法调用创建单独的会话。

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

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

【问题讨论】:

  • 在 withTransaction 中你应该调用 userDao.merge(user) 来查看 user.getName() 的变化
  • 我不明白打电话给userDao.merge(user) 会有什么帮助。调用withTransaction() newUserName 后正确插入数据库。问题是 withoutTransaction() 第二次调用 user.getName() 使用休眠一级缓存(因为 @Transactional(propagation = Propagation.NEVER) 创建休眠会话)而不是获取 newUserName 直接表单数据库。
  • hibernate 总是使用一级缓存,除非你清除它。
  • userDao.load 是事务方法吗?
  • 您好,您可以发布 Hibernate 配置吗?

标签: java spring hibernate jpa


【解决方案1】:

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

我引用Hibernate's official documentation:

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

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

可以通过调用save()persist()saveOrUpdate() 使瞬态实例持久化。可以通过调用delete() 使持久实例变为瞬态。 get()load() 方法返回的任何实例都是持久的。

由 Christian Bauer、Gavin King 和 Gary Gregory 取自 Java Persistence With Hibernate, Second Edition

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

同样来自Java Persistence With Hibernate, Second Edition

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

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

关于交易,这里摘自official Hibernate's documentation

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

所以,综上所述,withTransactionwithoutTransaction 不会共享UnitOfWork,因此不会共享一级缓存,这就是为什么第二次加载返回原始值的原因。

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

编辑:

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

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

就保护而言 - 尝试通过使 withTransaction 使用 Propagation.NEVER 和 withoutTransaction 使用默认的 @Transactional 注释来交换两种方法上的注释,看看会发生什么(剧透:你会得到一个 @ 987654344@).

EDIT2:

至于为什么会话在外部 bean 中的两个负载之间共享 - 这正是 JpaTransactionManager 应该做的事情,并且通过使用 @Transactional 注释您的方法,您已经通知 Spring 它应该使用配置的事务经理来包装你的方法。以下是the official documentationJpaTransactionManager 预期行为的评价:

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

另外,要了解 Spring 如何处理声明式事务管理(即方法上的 @Transactional 注释),请参阅 official documentation。为了便于导航,我将引用一段话:

关于 Spring 框架的声明式事务支持,最重要的概念是这种支持是通过 AOP 代理启用的,并且事务建议是由元数据驱动的(目前是基于 XML 或基于注释的)。 AOP 与事务元数据的组合产生了一个 AOP 代理,它使用 TransactionInterceptor 和适当的 PlatformTransactionManager 实现来驱动围绕方法调用的事务

【讨论】:

  • 我可以想象实现 @Transactional(propagation = Propagation.NEVER) 只保护持久的方法(例如 IO 处理)以便从事务上下文中调用(没有额外的副作用 - 创建会话)。在我的帖子中查看跟踪日志。
  • 日志准确地说明了答案所声称的 - 行为是正确的。如果您删除 @Transactional(propagation = Propagation.NEVER),则意味着不会创建任何事务,因此每个 DAO 调用都会导致创建一个新会话,这正是您的情况所发生的情况,正如日志所验证的那样
  • 至于你的例子——如果你只做 IO 相关的工作,你为什么需要数据库事务?您将执行哪些与数据库相关的工作? :)
  • 我知道hibernate需要会话来执行他的工作。我唯一要解释的是@Transactional(propagation = Propagation.NEVER) 不应该创建会话(在我看来)。因此,您可以想象 TransactionInterceptor 的假设实现,以防 @Transactional(propagation = Propagation.NEVER) 根本不创建会话。
  • 我不同意 - 如果您将方法声明为 @Transactional,那么您就是在告诉 Spring 您的方法应该与配置的事务管理器交互。在这种情况下,配置了一个 JpaTransactionManager 并且它完全按照它的配置来执行。如果您不希望您的方法与事务管理器进行任何交互,特别是如果您的方法确实有效但绝不是事务性的,那么您首先不应该对其进行注释。我还说该示例的行为就像那里的每份文档都说应该的那样。
【解决方案2】:

首先,当您在 JPA API 后面使用 hibernate 时,我将使用术语 EntityManager 而不是会话(完全一样,只是术语问题)。

每次使用 JPA 访问数据库都将涉及一个 EntityManager,您正在获取实体,您需要一个 EntityManager (EM)。所谓一级缓存,无非就是 EM 托管实体的状态。

理论上,EM 的生命周期很短,并且绑定到一个工作单元(因此通常绑定到一个事务,请参阅 Struggling to understand EntityManager proper use)。

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

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

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

【讨论】:

    【解决方案3】:

    @Transactional(propagation = Propagation.NEVER) 仍会创建会话。如果您将 Spring/Hibernate/JPA 组合用于非分布式事务,那么您肯定会使用 JpaTransactionManager 作为 Spring 事务管理器。你的问题的答案就在这堂课上。一个好主意是在您的 IDE 中使用调试器来跟踪正在发生的事情。这个类的doBegin方法(被Spring事务基础设施调用的是:-

    protected void doBegin(Object transaction, TransactionDefinition definition) {
            JpaTransactionObject txObject = (JpaTransactionObject) transaction;
    
            if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
                throw new IllegalTransactionStateException(
                        "Pre-bound JDBC Connection found! JpaTransactionManager does not support " +
                        "running within DataSourceTransactionManager if told to manage the DataSource itself. " +
                        "It is recommended to use a single JpaTransactionManager for all transactions " +
                        "on a single DataSource, no matter whether JPA or JDBC access.");
            }
    
            try {
                if (txObject.getEntityManagerHolder() == null ||
                        txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) {
                    EntityManager newEm = createEntityManagerForTransaction();
                    if (logger.isDebugEnabled()) {
                        logger.debug("Opened new EntityManager [" + newEm + "] for JPA transaction");
                    }
                    txObject.setEntityManagerHolder(new EntityManagerHolder(newEm), true);
                }
    
                EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
    
                // Delegate to JpaDialect for actual transaction begin.
                final int timeoutToUse = determineTimeout(definition);
                Object transactionData = getJpaDialect().beginTransaction(em,
                        new DelegatingTransactionDefinition(definition) {
                            @Override
                            public int getTimeout() {
                                return timeoutToUse;
                            }
                        });
                txObject.setTransactionData(transactionData);
    
                // Register transaction timeout.
                if (timeoutToUse != TransactionDefinition.TIMEOUT_DEFAULT) {
                    txObject.getEntityManagerHolder().setTimeoutInSeconds(timeoutToUse);
                }
    
                // Register the JPA EntityManager's JDBC Connection for the DataSource, if set.
                if (getDataSource() != null) {
                    ConnectionHandle conHandle = getJpaDialect().getJdbcConnection(em, definition.isReadOnly());
                    if (conHandle != null) {
                        ConnectionHolder conHolder = new ConnectionHolder(conHandle);
                        if (timeoutToUse != TransactionDefinition.TIMEOUT_DEFAULT) {
                            conHolder.setTimeoutInSeconds(timeoutToUse);
                        }
                        if (logger.isDebugEnabled()) {
                            logger.debug("Exposing JPA transaction as JDBC transaction [" +
                                    conHolder.getConnectionHandle() + "]");
                        }
                        TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
                        txObject.setConnectionHolder(conHolder);
                    }
                    else {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Not exposing JPA transaction [" + em + "] as JDBC transaction because " +
                                    "JpaDialect [" + getJpaDialect() + "] does not support JDBC Connection retrieval");
                        }
                    }
                }
    
                // Bind the entity manager holder to the thread.
                if (txObject.isNewEntityManagerHolder()) {
                    TransactionSynchronizationManager.bindResource(
                            getEntityManagerFactory(), txObject.getEntityManagerHolder());
                }
                txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true);
            }
    
            catch (TransactionException ex) {
                closeEntityManagerAfterFailedBegin(txObject);
                throw ex;
            }
            catch (Throwable ex) {
                closeEntityManagerAfterFailedBegin(txObject);
                throw new CannotCreateTransactionException("Could not open JPA EntityManager for transaction", ex);
            }
        }
    

    如您所见,使用 JPA 时的事务资源实际上是实体管理器(底层实现是休眠中的会话),这是该方法所做的第一件事

    EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
    

    所以肯定会创建一个实体管理器/会话。然后事务属性通过 TransactionDefinition 传递给底层的 JpaDialect (HibernateJpaDialect)。这个类又实际获取了底层的Hibernate Session和session的事务API。

    HibernateJpaDialect {
    ........
    public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition)
    Session session = getSession(entityManager);
    entityManager.getTransaction().begin();
    ......
    ......
    }
    ......
    

    【讨论】:

      【解决方案4】:

      我认为这不是正确的行为。同事们说的是真的,即使没有事务,休眠也会创建一个会话。但这意味着我们面临两个会话 S1 和 S2,用于来自 DAO 的两个单独读取。同时,L1 缓存始终是每个会话的,因此两个单独的会话对 L1 缓存的命中是没有意义的。看来您的 Spring 不尊重 @Transactional(propagation = Propagation.NEVER)

      @Transactional(propagation = Propagation.NEVER) 应该等同于您只是从主方法初始化服务并自己执行对 DAO 的后续调用。

      在主类中尝试一下,看看它会如何反应。我怀疑它会再次命中 L1 缓存。

      我还将从 Sprint 复制粘贴文档,从不传播:

      NEVER 非事务性执行,如果有事务则抛出异常 存在。

      还有一个问题 - hibernate 是否配置为 AutoCommit。是否有可能“runInTransaction” - 方法没有提交?

      【讨论】:

      • 完全不正确。首先,您可以在日志中看到withoutTransaction 没有发起任何事务,所以我看不出您在说哪两个事务。将有两个会话——一个在外部 bean 中,一个在内部 bean 中——每个会话都有自己的 L1 缓存。
      • @Transactional(propagation = Propagation.NEVER) 告诉 Spring 您的代码应该与配置的事务管理器进行交互和管理,然后描述交互的类型 - 即任何事务都不应该启动,并且不应允许此方法在具有自己的事务的另一个方法中执行。如果您不希望这种类型的行为,只需省略注释,您将获得您所描述的行为。
      • 作为开发人员,您应该知道您配置了哪个事务管理器以及与事务管理器的交互将产生什么结果。如果您阅读 JpaTransactionManager 的文档,您将看到观察到的行为是正确的
      • 对不起,我的意思是会话而不是交易,这是我的阅读障碍。我同意你的第一条评论,但我不同意第二条评论。
      • 嗯,这当然是你的权利。但是,虽然我们可能会讨论我们希望 Spring 支持和实现什么行为,但实际上没有讨论它当前应该做什么以及如何做的空间。我已经用 Spring 的官方文档证实了我的论点,很明显这就是他们所设想的工作方式。它可能不是您希望它做的事情,但它正在做完全官方文档说它应该做的事情。所以不,这不是不正确的行为。
      猜你喜欢
      • 2011-07-25
      • 2017-12-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-12-22
      • 2019-07-10
      • 2012-10-15
      • 2014-09-22
      相关资源
      最近更新 更多