Reputation: 759
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();
}
Upvotes: 4
Views: 5953
Reputation: 26064
Before full explaination a little note: try to post code that actually compiles and works as advertised.
main()
does not compile, 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.
childA
)In the second run
childA
)SessionImpl.getEntityUsingInterceptor
@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();
}
}
Upvotes: 0
Reputation: 973
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.
Upvotes: 1