Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring-Data JPA: save new entity referencing existing one

The question is basically the same as below one:

JPA cascade persist and references to detached entities throws PersistentObjectException. Why?

I'm creating a new entity that references an existing, detached one. Now when I save this entity in my spring data repository an exception is thrown:

org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist

if we look at the save() method in source code of spring data JPA we see:

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

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

and if we look at isNew() in AbstractEntityInformation

public boolean isNew(T entity) {

    return getId(entity) == null;
}

So basically if i save() a new entity (id == null), spring data will always call persist and hence this scenario will always fail.

This seems to be a very typical use case when adding new items to collections.

How can I resolve this?

EDIT 1:

NOTE:

This issue is NOT directly related to How to save a new entity that refers existing entity in Spring JPA?. To elaborate assume you get the request to create the new entity over http. You then extract the information from the request and create your entity and the existing referenced one. Hence they will always be detached.

like image 923
beginner_ Avatar asked May 15 '13 07:05

beginner_


3 Answers

I had a similar issue where I was trying to save an new entity object with an already saved entity object inside.

What I did was implemented Persistable< T > and implemented isNew() accordingly.

public class MyEntity implements Persistable<Long> {

    public boolean isNew() {
        return null == getId() &&
            subEntity.getId() == null;
    }

Or you could use AbstractPersistable and override the isNew there ofcourse.

I don't know if this will be considered a good way of handling this issue but it worked out quite good for me and besides feels very natural to do.

like image 196
Jonas Geiregat Avatar answered Nov 12 '22 23:11

Jonas Geiregat


The best I came up with is

public final T save(T containable) {
    // if entity containable.getCompound already exists, it
    // must first be reattached to the entity manager or else
    // an exception will occur (issue in Spring Data JPA ->
    // save() method internal calls persists instead of merge)
    if (containable.getId() == null
            && containable.getCompound().getId() != null){
        Compound compound = getCompoundService()
                .getById(containable.getCompound().getId());
        containable.setCompound(compound);   
    }
    containable = getRepository().save(containable);
    return containable; 
}

We check if we are in the problematic situation and if yes just reload the existing entity from the database by its id and set the field of the new entity to this freshly loaded instance. It then will be attached.

This requires that the service for new entity holds a reference to the service of the referenced entity. This should not be an issue since you are using spring anyway so that service can just be added as a new @Autowired field.

Another issue however (in my case this behavior is actually desired) that you can't change the referenced existing entity at the same time while saving the new one. All those changes will be ignored.

IMPORTANT NOTE:

In many and probably your cases this can be much simpler. You can add a reference of entity manager to your service:

@PersistenceContext
private EntityManager entityManager;

and in above if(){} block use

containable = entityManager.merge(containable);

instead of my code (untested if it works).

In my case the classes are abstract and targetEntity in @ManyToOne is hence abstract too. Calling entityManager.merge(containable) directly then leads to an exception. However if your classes are all concrete this should work.

like image 25
beginner_ Avatar answered Nov 12 '22 22:11

beginner_


I have the same problem with a @EmbeddedId and business data as part of the id.
The only way to know if the entity is new is performing a (em.find(entity)==null)?em.persist(entity):em.merge(entity)

but spring-data only provides save() method and there is no way to fill the Persistable.isNew() method with a find() method.

like image 4
nakkun Avatar answered Nov 12 '22 22:11

nakkun