Spring 数据 JPA - Hibernate - 更新具有瞬态嵌套子项的现有实体时的 TransientObjectException [英] Spring data JPA - Hibernate - TransientObjectException when updating an existing entity with transient nested children

查看:25
本文介绍了Spring 数据 JPA - Hibernate - 更新具有瞬态嵌套子项的现有实体时的 TransientObjectException的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我的第一个问题之后,问题发生了变化,所以我创建了一个新问题:org.hibernate.TransientObjectException 使用 CascadeType.ALL 持久化嵌套子项

Following my first question here, the question has changed so I'm creating a new one : org.hibernate.TransientObjectException persisting nested children with CascadeType.ALL

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

I found that my problem was not saving a new entity but updating an existing one.

让我们从头开始.

我有一个名为 Human 的类,其中包含一个狗列表:

I have a class called Human which has a list of dogs :

@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()));

    ...
}

狗类狗有一个小狗列表:

The dog class Dog has a list of puppies :

@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;
}

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

I'm trying to get an existing human that has a dog and the dog has a puppy, if I try to give him a new Dog with another set of puppies :

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 保存子项时自动保留子项.

In my understanding cascade = {CascadeType.ALL} should persist the children automatically when saving them with a CrudRepository.

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

The problem comes from the creation of a new dog with puppies when I'm updating an existing entity.

以下是我尝试过的工作示例:

Here are the working examples I tried :

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);

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

But the following one doesn't work whether the human I retrieve already has dogs or not :

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 将级联持久化到孩子和孩子的孩子.

Apparently, persisting a transient Human will cascade persist to the children and the children of the children.

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

Updating an existing Human will cascade persist to the children but not the children of the children and thus cause the TransientObjectException.

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

Is this expected behaviour? Am I supposed to use a separate repository to persist the dogs and puppies?

推荐答案

在 JPA 存储库上调用 save 将调用 persist 如果实体是瞬态或 合并 如果实体是分离的.

Calling save on a JPA repository will either call persist if the entity is transient or merge if the entity is detached.

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

If it calls persist, the persist will cascade and save all the children entities but not update existing ones.

如果它调用合并,合并操作将级联并合并所有具有 Id 的子级,但不会持久化没有 Id 的子级.

If it calls merge, the merge operation will cascade and merge all the children that have an Id, but it will not persist the ones that don't have an Id.

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

The Hibernate specific saveOrUpdate method seems to do this job. If anyone know of any other method available through JPA, please let me know.

编辑

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

I've actually managed to use spring repositories to save my entity. However I need to persist manually every new child and grandchild. For this I have written a method that uses the reflection 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);
            }
        }
    }
}

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

So this is how my update method looks:

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

The copyProperties method also uses reflection to copy all the properties. It uses the clear and addAll method on OneToMany relations.

这篇关于Spring 数据 JPA - Hibernate - 更新具有瞬态嵌套子项的现有实体时的 TransientObjectException的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆