Keeping a bidirectional parent-child collection up to date - java

I am trying to keep the collections of the entities up to date with the internal database structure but failing to do so with a bidirectional, cascade-delete relation between Parent and Child.
Deleting a parent should cascade-delete all children
Addition and deletion of a child should be reflected in the parent's getChildren() set
The code below works if there is only one child, any more than that and I get ConcurrentModificationException, which is logical since Hibernate iterates over the collection when cascading.
If I remove the #PreRemove the removeChild test below fails.
Any suggestions on how to solve this without adding a specific deleteChild method that performs the clean up? I am trying to avoid having any clean-up methods outside of the entities.
#Entity
public class Parent {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "parent", cascade = CascadeType.REMOVE)
private Set<Child> children = new HashSet<>();
public Set<Child> getChildren() {
return Collections.unmodifiableSet(children);
}
void internalAddChild(final Child child) {
children.add(child);
}
void internalRemoveChild(final Child child) {
children.remove(child);
}
}
#Entity
public class Child {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "parent_id", nullable = false)
private Parent parent;
public Child(final Parent parent) {
setParent(parent);
}
public final void setParent(final Parent parent) {
if (this.parent != null) {
this.parent.internalRemoveChild(this);
}
this.parent = parent;
if (parent != null) {
parent.internalAddChild(this);
}
}
#PreRemove
private void preRemove() {
// Causes ConcurrentModificationException in test removeParent below
if (parent != null) {
parent.internalRemoveChild(this);
}
}
}
Tests:
#Test
public void removeParent() {
EntityManager em = getEntityManager()
Parent parent = new Parent();
em.persist(parent);
em.persist(new Child(parent));
em.persist(new Child(parent));
assertTrue(parent.getChildren().size() == 2);
// Causes ConcurrentModificationException if more than 1 child
em.remove(parent);
// Both children should be deleted
}
#Test
public void removeChild() {
EntityManager em = getEntityManager()
Parent parent = new Parent();
em.persist(parent);
Child child = new Child(parent);
em.persist(child);
em.remove(child);
// Fails without #PreRemove in Child, child is still present in set
assertFalse(parent.getChildren().contains(child));
}
Exception stack trace:
java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
at java.util.HashMap$KeyIterator.next(HashMap.java:1453)
at org.hibernate.collection.internal.AbstractPersistentCollection$IteratorProxy.next(AbstractPersistentCollection.java:789)
at org.hibernate.engine.internal.Cascade.cascadeCollectionElements(Cascade.java:379)
at org.hibernate.engine.internal.Cascade.cascadeCollection(Cascade.java:319)
at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:296)
at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:161)
at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:118)
at org.hibernate.event.internal.DefaultDeleteEventListener.cascadeBeforeDelete(DefaultDeleteEventListener.java:353)
at org.hibernate.event.internal.DefaultDeleteEventListener.deleteEntity(DefaultDeleteEventListener.java:275)
at org.hibernate.event.internal.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:160)
at org.hibernate.event.internal.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:73)
at org.hibernate.internal.SessionImpl.fireDelete(SessionImpl.java:920)
at org.hibernate.internal.SessionImpl.delete(SessionImpl.java:896)
at org.hibernate.jpa.spi.AbstractEntityManagerImpl.remove(AbstractEntityManagerImpl.java:1214)
...

Try putting orphanRemoval = true on #OneToMany mapping, and remove CascadeType.REMOVE since it is now redundant. This instructs the persistence provider to remove child entities when parent is deleted, or their relation is set to null.
One side note (may affect this problem but doesn't have to, it's just good practice) is to avoid wiring up the relations in the constructor (like you do now in Child, and instead move the logic to some kind of addChild and removeChild methods (internalRemoveChild and internalAddChild in your case). It would look like this
void internalAddChild(final Child child) {
if (child != null) {
child.setParent(this);
children.add(child);
}
}
void internalRemoveChild(final Child child) {
if (child != null) {
children.remove(child);
child.setParent(null);
}
}
// test code
Parent parent = new Parent();
Child c1 = new Child();
Child c2 = new Child();
parent.internalAddChild(c1);
parent.internalAddChild(c2);
em.persist(parent);
em.persist(c1);
em.persist(c2);

Related

Hibernate: Child id is null after merging parent

I've poured over similar questions on stackoverflow but I'm not able to find the right answer, so I'll post the question, if I may:
I have a typical one-to-many Parent-Child entities in a Spring Boot application. So, I create a new child, add it to the parent, and merge/flush/clear entity manager. Child entity gets saved in the database correctly, with a new ID, but the Child object's ID under the Parent object is zero. What am I missing that would force the refresh of the ID?
The code that creates a new child is in the same controller that fetches the Parent initially, so it's using the same DAO and the same entity manager.
Here's my code (entities are mostly generated by JPA):
Parent:
#Entity
public class Parent implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
#OneToMany(mappedBy="parent", fetch=FetchType.EAGER, cascade={CascadeType.MERGE})
private List<Child> children;
public List<Child> getChildren() {
return this.Children;
}
public void setChildren(List<Child> children) {
this.children = children;
}
public Child addChild(Child child) {
getChildren().add(child);
child.setParent(this);
return child;
}
}
Child:
#Entity
public class Child implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
#ManyToOne(cascade={CascadeType.MERGE})
#JoinColumn(name="parent_id")
private Parent parent;
public Parent getParent() {
return this.parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
}
DAO:
#Repository
public class ParentDao {
#PersistenceContext
private EntityManager entityManager;
#Transactional
public void update(Parent parent) {
entityManager.merge(parent);
entityManager.flush();
entityManager.clear();
}
}
Controller:
#Autowired
private ParentDao parentDao;
. . . createChild() {
Child child = new Child();
// Set the link on both sides of the relationship
parent.addChild(child);
parentDao.update(parent);
// --> child.getId() == 0
}
The way merge() works when it is invoked on a new entity (Child in your case) it acts a persist() call with one small difference.
It only creates a copy of the passed entity and calls persist() on the copy.
So your Parent entity cannot see the change of id because it is holding the 'old' instance of the Child which was not persisted and eventually updated with a fresh id.
Hope that clears things up.
Update
To get the new state you need to:
1) change the transactional repository method to return the merge() value:
#Transactional
public Parent update(Parent parent) {
Parent newParent = entityManager.merge(parent);
entityManager.flush();
entityManager.clear();
return newParent
}
2) change your service:
#Autowired
private ParentDao parentDao;
. . . createChild() {
Child child = new Child();
// Set the link on both sides of the relationship
parent.addChild(child);
Parent newParent = parentDao.update(parent);
// --> newParent.getChildren(0).getId() == 0
}

Hibernate - Transferring a Collection of objects from one parent object to another

I have 3 types of objects - Parent, Child, and ChildAttr
My goal is to transfer a subset of Children from one Parent to another using Hibernate (3.2.5).
Objects are structured as such:
public class Parent {
Set<Child> children;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "parent")
#Cascade( { org.hibernate.annotations.CascadeType.SAVE_UPDATE, org.hibernate.annotations.CascadeType.DELETE_ORPHAN })
public Set<Child> getChildren() {
return this.children;
}
public void setChildren(Set<Child> children) {
this.children = children;
}
}
...
public class Child {
Set<ChildAttr> attributes;
Parent parent;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "child")
#Cascade( { org.hibernate.annotations.CascadeType.SAVE_UPDATE, org.hibernate.annotations.CascadeType.DELETE_ORPHAN })
public Set<ChildAttr> getAttributes() {
return this.attributes;
}
public void setAttributes(Set<ChildAttr> attributes) {
this.attributes = attributes;
}
}
...
public class ChildAttr {
Child child;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "child_id", nullable = false)
public Child getChild() {
return this.child;
}
public void setChild(Child child) {
this.child = child;
}
}
Now say I run some client code that takes a subset of Parent A's Child objects and moves them to Parent B:
Set<Child> children = getChildrenToTransfer(transferCriteria, parentA);
parentA.getChildren().removeAll(children);
manager.saveOrUpdate(parentA); // method also calls flush();
parentB.getChildren().addAll(children);
manager.saveOrUpdate(parentB); // error thrown here.
I get an error thrown when trying to save parentB.
Found two representations of same collection: com.mycode.Child.attributes;
The application currently seems to do this work fine in multiple sessions - e.g. - some user comes along and removes the set of children, then some time later adds them to some other Parent. Moreover, I don't really understand why it's instantiating multiple versions of that attribute list when it should really just be one, even if the Parent changes.
What is causing the aforementioned error and how do I get around it?
One rule of thumb: ensure the objects are consistent as an object graph before trying to persist them... iow you're removing all the children from parent A and are adding them to parentB but you have not updated the parent link for those children.
So I'd suggest the following:
add methods to Parent:
add(Child child) {
child.setParent0(this);
children.add(child);
}
remove(Child child) {
child.setParent0(null);
children.remove(child);
}
And then on Child:
setParent0(Parent parent) {
this.parent = parent;
}
setParent(Parent parent) {
parent.add(this);
}
This way when you add from either direction you've got a consistent object model without the outside code knowing the details.
It makes more sense to remove from the parent...
So try with these methods.
Seens thats because your bidirectional relation (parent-children). When you remove/add the child to one parent to another, you should update the parent reference.

Managing a simple OneToMany entity relationship in java without database

I'm trying to find a way to manage in a lightweight fashion two models that have a OneToMany relationship but will never be persisted.
My search has lead me to the javax.persistence that seems to be able to do what i want but i do not seem to be able to make it work.
As tests always say much more than a speech, here's what i'd like to achieve:
public void test_relationship() {
Parent p = new Parent("Mary");
Child c1 = new Child("Hugo");
Child c2 = new Child("Charly");
Child c3 = new Child("Françine");
p.addChild(c1)
Assert.assertEquals(p, c1.getParent());
p.removeChild(c1)
Assert.assertNull(c1.getParent());
p.addChildren(c1, c2)
Assert.assertEquals(p, c1.getParent());
Assert.assertEquals(p, c2.getParent());
c1.removeParent();
Assert.assertFalse(p.hasChild(c1));
c1.setParent(p);
Assert.assertTrue(p.hasChild(c1));
}
All this without a database. It's only purpose is to facilitate the access from one object to another. These objects will be built depending on data received as an HTTP request payload (JSON format) and will need to be serialized back to JSON as part of the response.
For now here's what i've done
#Entity
public class Parent implements Serializable {
private Collection<Child> children = new HashSet<Child>();
#Id
private String id;
#OneToMany(mappedBy = "parent", fetch = FetchType.EAGER)
public Collection<Child> getChildren() {
return children;
}
public void addChild(child) {
this.children.add(child);
}
public void removeChild(child) {
this.children.remove(child);
}
public boolean hasChild(child) {
this.children.contains(child);
}
}
#Entity
public class Child implements Serializable {
private Parent parent;
#Id
private String id;
#ManyToOne
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
public void removeParent() {
this.parent = null;
}
}
Edit:
I'm expecting the relationship behavior to come from javax.persistence. Maybe through an EntityManager ? I'm really confused on how those work. I know I could code this behavior myself but I would like to avoid to do this on each relation of each of my entities and have the driest code possible.
You are part of the way there. The bit that is missing is that when you create/update/remove one side of a Parent <-> Child relationship you need to do the same on the other side.
So for example
public void setParent(Parent parent) {
if (this.parent != null) {
this.parent.removeChild(this);
}
this.parent = parent;
parent.addChild(this);
}
(And change childs to children or the grammar monster will eat you!)

Hibernate - "detached entity passed to persist" error for already persisted child

I have an entity, which is already persisted and want to add it to a newly generated parent entity (not yet persisted). If i try to persisted the parent then, i get the error "detached entity passed to persist: model.Child". I think i have to somehow call a "entityManager.merge()" for the child instead of a "entityManager.persist()". But i do not explicitly call the persist. This is handled by the "cascade = CascadeType.ALL" annotation. Can i tell hibernate somehow to do a merge here, if the entity already exists?
By the way: If i first persist the parent, then add the child and then persist the parent again -> It works (But makes my application logic much more complicated).
Here my code:
public class App
{
#Test
public void test()
{
// I have a child object (in the real app
//this is a user object and already persisted
Child child = new Child();
HibernateHelper.persist(child);
Parent parent = new Parent();
parent.addChildren(child);
// throws the exception "detached entity passed to persist: model.Child"
HibernateHelper.persist(parent);
Parent newParent = HibernateHelper.find(Parent.class, parent.getId());
assertEquals(1, newParent.getChildren().size());
}
}
My "child" entity:
#Entity
#Table(name = "child")
public class Child {
public Child(){}
private Long id;
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
private Parent parent;
#ManyToOne
#JoinColumn
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
}
My "parent" entity:
#Entity
#Table(name="parent")
public class Parent {
public Parent(){}
private Long id;
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
private Set<Child> children = new HashSet<Child>();
#OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
public Set<Child> getChildren() {
return children;
}
public void setChildren(Set<Child> children) {
this.children = children;
}
public void addChildren(Child child){
children.add(child);
child.setParent(this);
}
}
The persist helper method (looks the same for the child)
public static void persist(Parent entity){
EntityManager entityManager = null;
try {
entityManager = beginTransaction();
if(entity.getId()!=null){
entityManager.merge(entity);
}else{
entityManager.persist(entity);
}
entityManager.getTransaction().commit();
} catch (Exception e) {
System.out.println(e);
return;
}finally{
if(entityManager != null)
entityManager.close();
}
}
An option would be to always use EntityManager.merge. In case a new entity is passed it is persisted, in case a detached entity is passed it is merged into the current persistence context.

JPA: Reverse cascading delete

#Entity
public class Parent {
#Id
#GeneratedValue(strategy=GenerationType.TABLE)
int id;
#OneToMany(cascade=CascadeType.REMOVE)
List<Item> children = new ArrayList<Child>();
}
#Entity
public class Child {
#Id
#GeneratedValue(strategy=GenerationType.TABLE)
int id;
}
As you can see above, I have a OneToMany-Relation between parent and child. If I delete an instance of parent, all children are also deleted. Is there a way to get this working the other way round as well?
Parent p = new Parent();
Child c = new Child();
p.children.add(c);
EntityManager.persist(p);
EntityManager.persist(c);
EntityManager.remove (c);
This code runs without exception, but when I load p the next time, there is a new child attached.
If you want deletes to work from both sides, you need to define a bi-directional relationship between Parent and Child:
// in Parent
#OneToMany(cascade=CascadeType.REMOVE, mappedBy="parent")
List<Item> children = new ArrayList<Child>();
// in Child
#ManyToOne
Parent parent;

Categories

Resources