A different object with the same identifier using Spring data jpa with Hibernate

HKIT :

I have checked different sources but none solve my problem, such as: https://coderanch.com/t/671882/databases/Updating-child-DTO-object-MapsId

Spring + Hibernate : a different object with the same identifier value was already associated with the session

My case: I have created 2 classes, 1 repository as below:

@Entity
public class Parent{
  @Id
  public long pid;

  public String name;

  @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
  public List<Child> children;
}

-------------------------------------------------------------------

@Entity
public class Child{
  @EmbeddedId
  public PK childPK = new PK();

  public String name;

  @ManyToOne
  @MapsId("parentPk")
  @JoinColumn(name = "foreignKeyFromParent")
  public Parent parent;

  @Embeddable
  @EqualsAndHashCode
  static class PK implements Serializable {
      public long parentPk;
      public long cid;
  }
}
------------------------------------------------------------------------

public interface ParentRepository extends JpaRepository<AmazonTest, Long> {
}

Where Parent and Child has One To Many relationship. In my main method:

public static void main(String[] args) {
    @Autowired
    private ParentRepository parentRepository;

    Parent parent = new Parent();
    parent.pid = 1;
    parent.name = "Parent 1";

    Child child = new Child();
    List<Child> childList = new ArrayList<>();

    child.childPK.cid = 1;
    child.name = "Child 1";
    childList.add(child);

    parent.children= childList;

    parentRepository.save(parent);
    parentRepository.flush();
}


When I run the application for the first time, data can successfully saved to the database. But if I run it again, it gives error "Exception: org.springframework.dao.DataIntegrityViolationException: A different object with the same identifier value was already associated with the session".
I was expecting if the data is new, it will update my database, if data is the same, nothing happen. What's wrong with my code.

If I made parent stand alone (without any relationship with the child). It will not give any error even I rerun the application.

Edited: However, if I use the below implementation with simple primary key in Child Entity, it will work as I expected. I can rerun the application without error. I can also change the value, such as the child.name and it will reflect in database.

@Entity
public class Parent{
   @Id
   public long pid;

   public String name;

   @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
   public List<Child> children;
}

-------------------------------------------------------------------

@Entity
public class Child{
   @Id
   public long cid;


   public String name;

   @ManyToOne
   @JoinColumn(name = "foreignKeyFromParent")
   public Parent parent;

}
------------------------------------------------------------------------

public interface ParentRepository extends JpaRepository<AmazonTest, Long> {
}

-------------------------------------------------------------------------
public static void main(String[] args) {
   @Autowired
   private ParentRepository parentRepository;

   Parent parent = new Parent();
   parent.pid = 1;
   parent.name = "Parent 1";

   Child child = new Child();
   List<Child> childList = new ArrayList<>();

   child.cid = 1;
   child.name = "Child 1";
   childList.add(child);

   parent.children= childList;

   parentRepository.save(parent);
   parentRepository.flush();
}
Lesiak :

Before full explaination a little note: try to post code that actually compiles and works as advertised.

  • Your main() does not compile,
  • you dont set up full relation between Parent and Child.
  • Also try to explicitely demarcate transactions in the posted example.

How your code works

You are calling save on a repository. Underneath, this method calls entityManager.merge() as you have set an id yourself. Merge calls SQL Select to verify if the object is there, and subsequently calls SQL insert or update for the object. (The suggestions that save with the object with id that exists in db are wrong)

  • In the first run, the object is not there.

    • you insert parent
    • merge is cascaded and you insert child (lets call it childA)
  • In the second run

    • merge selects parent (with childA)
    • We compare if new parent is already in the session. This is done in SessionImpl.getEntityUsingInterceptor
    • parent is found
    • merge is cascaded to the child
    • again, we check if the object is already in the session.
    • Now the difference comes:
    • Depending on how you set up the relation between child and parent, the child may have an incomplete PK (and rely on filling it from the relation to parent annotated with @MapsId). Unfortunately, the entity is not found in the session via the incomplete PK, but later, when saving, the PK is complete, and now, you have 2 confilicting objects with the same key.

To solve it

Child child = new Child();
child.parent = parent;
child.childPK.cid = 1;
child.childPK.parentPk = 1;

This also explains why the code works when you change the PK of Child to a long - there is no way to screw it up and have an incomplete PK.

NOTE

The solution above makes mess with orphans.

I still think that the original solution is better as the orphans are removed. Also, adding updated soution to original solution is a worthwhile update. Removing entire list and re-inserting it is not likely perform well under load. Unfortunalely it removes the list on the first merge of the parent, and re-adds them on the second merge of the parent. (This is why clear is not needed)

Better still, just find the parent entity and make the updates on it (as other answers suggest).

Even better, try to look at the solution and add / replace only specific children of the parent, not lookig at the parent and its children ollection. This will be likely most performant.

Original Solution

I propose the following (note that total replacement of the chilren list is not allowed, as it is a hibernate proxy).

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
public List<Child> children = new ArrayList<>();
@SpringBootTest
public class ParentOrphanRepositoryTest {

    @Autowired
    private ParentOrphanRepository parentOrphanRepository;

    @Test
    public void testDoubleAdd() {
        addEntity();
        addEntity();
    }

    @Transactional
    public void addEntity() {
        Parent parent = new Parent();
        parent.pid = 1;
        parent.name = "Parent 1";

        parent = parentOrphanRepository.save(parent);


        Child child = new Child();
        List<Child> childList = new ArrayList<>();

        child.parent = parent;
        child.childPK.cid = 1;
        child.name = "Child 1";
        childList.add(child);

        // parent.children.clear(); Not needed.
        parent.children.addAll(childList);
        parentOrphanRepository.save(parent);
        parentOrphanRepository.flush();
    }
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=105414&siteId=1