I have checked different sources but none solve my problem, such as: https://coderanch.com/t/671882/databases/Updating-child-DTO-object-MapsId
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();
}
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.
- merge selects parent (with
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();
}
}