【问题标题】:Spring data JPA - Hibernate - TransientObjectException when updating an existing entity with transient nested childrenSpring data JPA - Hibernate - TransientObjectException 更新具有瞬态嵌套子级的现有实体时
【发布时间】:2020-06-09 08:20:21
【问题描述】:

在我在这里的第一个问题之后,问题已经改变,所以我正在创建一个新问题:org.hibernate.TransientObjectException persisting nested children with CascadeType.ALL

我发现我的问题不是保存新实体,而是更新现有实体。

让我们从头开始。

我有一个名为 Human 的类,它有一个狗列表:

@Entity
public class Human {

    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(fetch = FetchType.LAZY, cascade = {CascadeType.ALL}, orphanRemoval = true)
    private Set<Dog> dogs = new HashSet<>(List.of(new Dog()));

    ...
}

狗类 Dog 有一个小狗列表:

@Entity
public class Dog {

    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL}, orphanRemoval = true)
    private Set<Puppy> puppies = new HashSet<>(List.of(new Puppy()));
}
@Entity
public class Puppy {

    @Id
    @GeneratedValue
    private Long id;
}

如果我尝试给他一个新的狗和另一组小狗,我正在尝试让一个现有的人养一只狗,而狗也养一只小狗:

Human human = humanRepository.findById(id); // This human already had a dog and the dog has puppies
Set<Dog> dogs = new HashSet<>();
Dog dog = new Dog();
dog.setPuppies(new HashSet<>(List.of(new Puppy())));
dogs.add(dog);
human.setDogs(dogs);
humanRepository.save(human);

我收到以下错误:

org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.test.Puppy

据我了解,cascade = {CascadeType.ALL} 应该在使用 CrudRepository 保存孩子时自动保留孩子。

编辑:

问题来自于我在更新现有实体时创建了一条带有小狗的新狗。

这是我尝试过的工作示例:

Human human = new Human();
Dog dog = new Dog();
Puppy puppy = new Puppy();
dog.getPuppies().clear();
dog.getPuppies().add(puppy);
human.getDogs().clear();
human.getDogs().add(dog);
humanRepository.save(human);
Human human = humanRepository.findById(id);
human.getDogs().clear();
human.getDogs().add(new Dog());
humanRepository.save(human);

但是无论我检索到的人是否已经有狗,以下方法都不起作用:

Human human = humanRepository.findById(id);
Dog dog = new Dog();
Puppy puppy = new Puppy();
dog.getPuppies().clear();
dog.getPuppies().add(puppy);
human.getDogs().clear();
human.getDogs().add(dog);
humanRepository.save(human);

显然,持久化一个瞬态 Human 将级联持久化到子级和子级的子级。

更新现有 Human 将级联持续到子级而不是子级的子级,从而导致TransientObjectException

这是预期的行为吗?我应该使用单独的存储库来保存狗和小狗吗?

【问题讨论】:

  • 不要这样做dog.setPuppies 和`human.setDogs(dogs);`。而是将其添加到已经存在的持久化和托管集合中(也将其删除)。
  • 所以human.getDogs().clear() 然后human.getDogs().add(dog);?我会尽力让你知道,谢谢!
  • 我仍然遇到同样的错误。我也尝试过清除 HashSet 中的小狗。 human.getDogs().forEach(e -&gt; e.getPuppies().clear());。我还必须将FetchType.EAGER 放在任何地方,否则我会收到另一个延迟初始化错误,而@Transactional 对此无济于事。
  • 您还需要将关系设置为空。否则它不是孤儿
  • 你的意思是像human.getDogs().forEach(dog -&gt; dog.getPuppies().forEach(puppy -&gt; puppy.setDog(null)))这样的东西吗?那是行不通的,因为小狗不引用狗。还是你的意思是别的?

标签: spring spring-data-jpa spring-data


【解决方案1】:

在 JPA 存储库上调用 save 如果实体是瞬态的,则将调用 persist;如果实体是分离的,则将调用 merge

如果它调用persist,persist将级联并保存所有子实体,但不会更新现有的。

如果调用merge,merge操作会级联合并所有有Id的children,但不会持久化没有Id的children。

Hibernate 特定的saveOrUpdate 方法似乎可以完成这项工作。如果有人知道通过 JPA 提供的任何其他方法,请告诉我。

编辑

我实际上已经设法使用 spring 存储库来保存我的实体。但是我需要手动坚持每个新的孩子和孙子。为此,我编写了一个使用反射 API 的方法!

void saveNewEntites(Object entity, EntityManager em) throws IllegalAccessException {
    Class<?> clazz = entity.getClass();
    for (Field field : clazz.getDeclaredFields()) {
        field.setAccessible(true);
        if (field.isAnnotationPresent(OneToMany.class)) {
            for(Object child : (Collection<?>)field.get(entity)){
                for(Field childField : child.getClass().getDeclaredFields()){
                    childField.setAccessible(true);
                    if(childField.isAnnotationPresent(Id.class) && childField.get(child) == null){
                        em.persist(child);
                        break;
                    }
                }
                saveNewEntites(child, em);
            }
        }
    }
}

这就是我的更新方法的样子:

@RequestMapping(method = PATCH, path = "/{id}")
@ApiResponse(responseCode = "204", description = "Entity updated")
@ApiResponse(responseCode = "400", description = "Data is invalid for update")
@Transactional
public ResponseEntity<?> update(@PathVariable Long id, @RequestBody @Valid ResourceDTO resource) {

    ResourceEntity entity = repository.findById(id).orElseThrow(ResourceNotFoundException::new);

    copyProperties(resource, entity);

    try {
        saveNewEntites(entity, em);
        repository.save(entity);
    } catch(Exception e){
        e.printStackTrace();
        throw new InvalidCommandException("Data is invalid for update.");
    }
    return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}

copyProperties 方法也使用反射来复制所有属性。它在 OneToMany 关系上使用 clearaddAll 方法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-10-04
    • 1970-01-01
    • 2022-06-11
    • 1970-01-01
    • 2013-05-09
    • 2021-05-21
    • 2016-01-28
    • 2021-07-28
    相关资源
    最近更新 更多