Sunday, January 13, 2008

Managing OneToMany relationship in JPA

Now classic book Java Persistence with Hibernate addresses OneToMany relationship in detail. However, I still struggled to find a way to maintain the list of children from the parent entity. The problem appeared while removing children from the list.

Example:
@Entity
@Table(name = "PARENT")
public class Parent implements Serializable {
...
@OneToMany(cascade = { CascadeType.PERSIST,
CascadeType.MERGE },
mappedBy = "parent")
private Set getChildren() {
return children;
}
...
}

@Entity
@Table(name = "CHILD")
public class Child implements Serializable {
...
@ManyToOne

@JoinColumn(name = "PARENTID",
nullable = true)

private Parent getParent() {
return parent;
}
...
}
The problem: I would like to maintain children by manipulating set of children held in parent entity in detached state. After new children are added and/or some of its existing children are removed, parent entity is updated (merged) and with it set of children is updated as well.

Note, that child entity may simply be unassigned from its parent to exist with no parent (nullable = true): by removing child from children list we are not removing child entity from persistent context. This case is more general than one when child is simply removed when unassigned from its parent.

Also note, that child class should implement equals() and hashCode() based on values that uniquely identify each instance. Of course, this is simply JPA best practice.

The following classic solution by the book handles adding new children flawlessly:
@Entity
@Table(name = "PARENT")
public class Parent implements Serializable {
...
@OneToMany(cascade = { CascadeType.PERSIST,
CascadeType.MERGE },
mappedBy = "parent")
private Set getChildren() {
return children;
}

public void addChild(Child theChild) {
theChild.setParent(this);
children.add(theChild);
}
...
}

public class ParentManager {
...
@Transactional(propagation = Propagation.REQUIRED,
readOnly = false)
public void updateParent(Parent parent) {
parentDAO.update(parent);
}

...
}

But if I decide to remove children from the list then this will not have any effect on database as merge operation on parent entity (update) will happily restore removed children. You can search various JPA forums on this topic, e.g.
http://jira.jboss.org/jira/browse/EJBTHREE-941
http://forum.java.sun.com/thread.jspa?threadID=5145294&tstart=210
http://forums.oracle.com/forums/thread.jspa?messageID=1707487

The implementation I propose is not confined to JPA entity classes - it requires coding at higher level of persistent context (where EntityManager is used). This is usually handled at session bean or business manager levels (depending on if I use EJB3 or POJO frameworks):

@Entity
@Table(name = "PARENT")
public class Parent implements Serializable {
...
@OneToMany(cascade = { CascadeType.PERSIST,
CascadeType.MERGE },
mappedBy = "parent")
private Set getChildren() {
return children;
}

@Transient
public Set getUnassignedChildren() {
return unassignedChildren;
}

public void addChild(Child theChild) {
theChild.setParent(this);
children.add(theChild);
}

public void unassignChild(Child theChild) {
if (children.remove(theChild)) {
theChild.setParent(null);
unassignedChildren.add(theChild);

}
}
...
}
And Parent manager update:
public class ParentManager {
...
@Transactional(propagation = Propagation.REQUIRED,
readOnly = false)
public void updateParent(Parent parent) {
if (!parent.getUnassignedChildren().isEmpty()) {
for (Child child : parent.getUnassignedChildren()) {
childManager.update(child);
}
}
parentDAO.update(parent);
}

...
}
Thus, any change to set of children using addChild() and unassignChild() is guaranteed to propagate to the database via single update to parent entity - problem solved.

No comments: