4

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();
}
HKIT
  • 628
  • 1
  • 8
  • 19

2 Answers2

1

Well, parent.pid is your database primary key. You can only save one recordset to the database with id=1. This is expected behaviour.

Maybe make yourself familiar with @GeneratedValue in order to avoid setting the id yourself.

Mick
  • 954
  • 7
  • 17
  • If I just save parent instance (without any relationhip with child) . Even I rerun the application again, it shows no error message, with the same id. I think hibernate has handled this. Edited this in my question – HKIT May 20 '19 at 09:40
  • Is parent.pid a primary key in your database? Do you allow multiple records with the same id? An @Id annotation in your Java code doesn't automatically make it primary in your database. It would explain what you are describing if not. – Mick May 20 '19 at 10:27
  • Yes, I have set pid as my primary key in my database – HKIT May 20 '19 at 10:35
0

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();
    }
}
Lesiak
  • 22,088
  • 2
  • 41
  • 65
  • Can you explain why you save the parent first, and then clear an empty array (I mean parent.children.clear())? – HKIT May 21 '19 at 07:14
  • Updated the answer to address your updated mapping. – Lesiak May 21 '19 at 10:41
  • `and then clear an empty array`. That is not true. After merge on parent, the array is a lazy proxy to all children that existed in the db. – Lesiak May 21 '19 at 10:43
  • Thanks a lot, detailed answers with good explanations. Got 2 more questions. I used the original solution without clear the array. It works. It actually create delete sql to clear the child table in db. why is like that? How can I inspect the actual element in the array(proxy to all children) – HKIT May 21 '19 at 12:44
  • Sorry for massive updates, but I really needed a debugger to check it. Updated again, it turns out the delete was issued on the first merge (parent has empty children, lets remove orphans), and there is no opportunity to merge the list from old parent with the list from new parent with this approach. finding the old Parent will give you such an opportunity. – Lesiak May 21 '19 at 13:48
  • Thank you for the downvote, can you explain? I am eager to learn what you consider controversial in my answer. – Lesiak May 21 '19 at 13:52
  • I upvoted you and chose your answer as the best answer. Thanks for the explanation. – HKIT May 21 '19 at 13:59
  • I am a bit confused what actually repository.save() does. When I create a new instance of Entity, and save it using repository.save(instance), I can saw in the console of select query. I am wondering after execute the select query, is the result from database being converted to entity and save to the persistent context so that we can check is our entity can be checked for existence in database? – HKIT May 21 '19 at 14:06
  • @HKIT I appreciate that, this was towards the person who downvoted. And to be clear: no offence taken, the answer was not of the top of my head but a result of inspecting SessionImpl with the debugger. I am eager to hear where I am wrong. – Lesiak May 21 '19 at 14:09
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/193706/discussion-between-lesiak-and-hkit). – Lesiak May 21 '19 at 14:09