Spring data JPA - Hibernate - TransientObjectException when updating an existing entity with transient nested children

99封情书 提交于 2020-06-17 09:44:07

问题


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.

Let's start from the beginning.

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

I get the following error :

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

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

EDIT:

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

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

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?


回答1:


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

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

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.

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

EDIT

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

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



来源:https://stackoverflow.com/questions/62278086/spring-data-jpa-hibernate-transientobjectexception-when-updating-an-existing

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!