【问题标题】:Behaviour of Hibernate merge() when @Transactional method throws an exception@Transactional 方法抛出异常时 Hibernate merge() 的行为
【发布时间】:2016-08-05 14:49:18
【问题描述】:

我想问一下这种行为的原因,因为当遇到 Spring @Transactional 方法/类时,我似乎不完全理解 Hibernate 中 persist()merge() 之间的区别。

我有以下代码应该回滚数据库操作但它没有(整个类被注释为@Transactional):

@Override
public MyBean assignNewFoo(Integer id, Integer idNewFoo) {

    MyBean bean = myBeanRepository.findOne(id);
    bean = myBeanRepository.save(bean);

    bean.setNewFoo(
            fooManagement.findById(idNewFoo)
            );
    if (true) throw new RuntimeException();
    return bean;
}

以下代码在抛出异常时按预期回滚:

@Override
public MyBean assignNewFoo(Integer id, Integer idNewFoo) {

    MyBean bean = myBeanRepository.findOne(id);
    myBeanRepository.save(bean);

    bean.setNewFoo(
            fooManagement.findById(idNewFoo)
            );
    if (true) throw new RuntimeException();
    return bean;
}

save()方法来自org.springframework.data.jpa.repository.support.SimpleJpaRepository类,所以它的代码是:

@Transactional
public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

该实体是现有的,所以我知道它正在执行merge()。根据JPA specification

find 方法(前提是它在没有锁的情况下调用或使用 LockModeType.NONE) 和 getReference 方法不需要 在事务上下文中调用。 如果实体经理有 事务范围的持久性上下文正在使用中,结果 实体将被分离;如果实体管理器具有扩展 使用持久化上下文,它们将被管理。


合并操作允许从分离的状态传播 实体到由实体管理器管理的持久实体上。这 应用于实体 X 的合并操作的语义为 如下:

  • 如果 X 是分离实体,则 X 的状态将复制到具有相同身份的预先存在的托管实体实例 X' 或新的 已创建 X 的托管副本 X'。
  • 如果 X 是一个新的实体实例,则创建一个新的托管实体实例 X',并将 X 的状态复制到新的托管实体中 实例 X'。
  • 如果 X 是已移除的实体实例,则合并操作将抛出 IllegalArgumentException(或事务提交将 失败)。
  • 如果 X 是一个托管实体,它会被合并操作忽略,但是,合并操作会级联到由 来自 X 的关系,如果这些关系已经用 级联元素值 cascade=MERGE 或 cascade=ALL 注释。
  • 对于由来自 X 的具有级联元素值 cascade=MERGE 或 cascade=ALL 的关系引用的所有实体 Y,Y 被合并 递归为 Y'。对于 X 引用的所有此类 Y,X' 设置为 参考 Y'。 (请注意,如果 X 是托管的,则 X 与 X'。)
  • 如果 X 是合并到 X' 的实体,并引用另一个实体 Y,其中未指定 cascade=MERGE 或 cascade=ALL,则 从 X' 导航相同的关联会产生对 a 的引用 具有与 Y 相同的持久标识的托管对象 Y'。
  1. 如果merge() 返回的副本应该是托管实体,为什么当我使用分离的实体时更改存储在数据库中? (除非有例外。这是我想要的行为)

  2. 如果我修改了新的托管实体但抛出异常,为什么仍然提交更改?

编辑应@alan-hay的要求:

package org.customer.somefoos.service.impl;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import org.customer.somefoos.entity.MyBean;
import org.customer.somefoos.repository.MyBeanRepository;
import org.customer.somefoos.service.MyBeanManagement;
import org.customer.somefoos.service.FooManagement;

@Service
@Transactional
public class MyBeanManagementImpl implements MyBeanManagement {

    @Resource
    private MyBeanRepository myBeanRepository;

    @Resource
    private FooManagement fooManagement;


    @Override
    public List<MyBean> findAll() {
        return myBeanRepository.findAll();
    }

    @Override
    public MyBean findById(Integer id) {
        return myBeanRepository.findOne(id);
    }

    @Override
    public void delete(Integer id) {
        myBeanRepository.delete(id);
    }

    @Override
    public MyBean save(MyBean myBean) { 
        return myBeanRepository.save(myBean);
    }

    @Override
    public MyBean assignNewFoo(Integer id, Integer idNewFoo) {

        MyBean bean = myBeanRepository.findOne(id);
        myBeanRepository.save(bean);

        bean.setNewFoo(
                fooManagement.findById(idNewFoo)
                );
        if (true) throw new RuntimeException();
        return bean;
    }

}

【问题讨论】:

  • 能把全班的代码贴出来吗?
  • 完成。 @AlanHay,您认为其他地方有什么问题吗?

标签: spring hibernate jpa transactional


【解决方案1】:
  1. 您似乎误解了合并语义和容器管理的事务行为。您的 assignNewFoo 方法是事务性的,并且您的“bean”实例是从存储库加载的。因此,“bean”实例一直被管理直到事务结束(或者直到您手动从持久性上下文中删除)。这意味着 myBeanRepository.save(bean); 调用什么也不做,因为“bean”已经是 JPA 管理的实体。 myBeanRepository.save(bean) == bean 将是真实的,只要保存是在同一个事务“findOne”已经发出。合并用于将对实体的非托管实例所做的更改应用于托管实例。此代码说明案例合并用于:

    MyBean bean = repo.findOne(id);
    MyBean anotherInstance = new MyBean();
    anotherInstance.setId(id);
    anotherInstance.setNewFoo("100");
    MyBean managed = repo.save(anotherInstance);
    // And now we take a look:
    managed == bean; // => true
    anotherInstance == managed; // => false
    bean.getNewFoo(); // => "100"
    // An anotherInstance is still detached while save() call has 
    // returned us a managed instance ('bean')
    

    根据您引用的 JPA Spec 条目:此处不适用。它说的是非事务性搜索,但您的搜索是在由assignNewFoo 调用启动的事务中执行的。

  2. 从上面写的所有内容来看:您提供的两个用于演示无回滚行为的代码示例实际上是相同的。您可能会遇到您抱怨的问题的一些原因:

    • 您从@Transactional 方法调用assignNewFoo,并在此外部@Transactional 方法中执行事务应用程序检查。由于您的传播级别是“必需的”并且在assignNewFoo 调用中未捕获到 RuntimeException,因此一旦assignNewFoo 调用完成,事务将被标记为回滚,但实际回滚将在您的事务已从中传播的方法完成后执行.
    • 如果您 100 % 确定您做的一切都是正确的,那么这可能是 Spring/Provider/DBMS 问题。我无法在最新的 Spring Boot + Hibernate 4 + HSQLDB 上重现此错误,如果您没有选择,可能值得检查。

【讨论】:

  • 感谢您的回复,罗曼。它非常有用。我进一步调试发现PersistenceContextType实际上是扩展的,而不是我想的事务,所以这是适用于findOne的文本:“[...]如果使用具有扩展持久性上下文的实体管理器,他们将被管理”,正如你所说。关于 2. 我已经无法再次重现这个,所以我不能说什么。
猜你喜欢
  • 1970-01-01
  • 2020-07-29
  • 2017-02-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-15
  • 1970-01-01
  • 2015-05-01
相关资源
最近更新 更多