Hello everyone,
I'm currently working with Hibernate 4.3.7. and JPA 2.1. What I need to do is to persist the following graph structure to a database via Hibernate:
- A Graph consists of a (lazy) set of Nodes
- A Node consists of a (lazy) set of neighbor nodes (and some primitive data which is irrelevant here)
You can find the source code at the very end of this post.
The lazy part is working well. However, I have two problems:
1) When I create a new Graph in my application with 1000 nodes or more and save the graph, I run into a
StackOverflowError. Some cascades seem to run into a cycle. However, it doesn't work without them.
2) In some scenarios, I need to load the
entire graph into memory, including all lazy references. How can I do this efficiently without executing N+1 SQL statements? I tried using a HQL fetch query, but I still get N+1 selects. I also tried to apply a FetchProfile, with the same result.
EDIT:
It turns out that for problem 1, this only happens if I call EntityManager.merge(graph) first. EntityManager.persist(graph) works fine.
EDIT 2:
The thing that actually causes trouble is a large graph with a cyclic reference somewhere, like A->B->C->...->XYZ->A. That seems to cause the merge stack overflow error.
Any help would be much appreciated, I'm really depending on being able to persist this structure using Hibernate.
Thank you,
Alan
The following is the source code that I am using.
Graph.javaCode:
@Entity
public class Graph extends PersistableObject {
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "graph")
private Set<Node> nodes = new HashSet<Node>();
// getters, setters...
}
Node.javaCode:
@Entity
public class Node extends PersistableObject {
@ManyToMany(fetch = FetchType.LAZY, cascade = { CascadeType.MERGE, CascadeType.PERSIST })
private Set<Node> neighbors = new HashSet<Node>();
@ManyToOne(fetch = FetchType.EAGER, cascade = { CascadeType.MERGE })
private Graph graph;
// getters, setters...
}
PersistableObject.javaCode:
@MappedSuperclass
public abstract class PersistableObject {
@Id
private String id = UUID.randomUUID().toString();
public String getId() {
return this.id;
}
public void setId(final String id) {
this.id = id;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (this.id == null ? 0 : this.id.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (this.getClass() != obj.getClass()) {
return false;
}
PersistableObject other = (PersistableObject) obj;
if (this.id == null) {
if (other.id != null) {
return false;
}
} else if (!this.id.equals(other.id)) {
return false;
}
return true;
}
}
For your convenience, here is a
GraphFactory that is capable of creating random graphs of given sizes.
Code:
public class GraphFactory {
public static Graph createRandomGraph(final int numberOfNodes, final int edgesPerNode) {
Graph graph = new Graph();
// we use this list for random index access
List<Node> nodes = new ArrayList<Node>();
for (int nodeIndex = 0; nodeIndex < numberOfNodes; nodeIndex++) {
Node node = new Node();
node.setGraph(graph);
graph.getNodes().add(node);
nodes.add(node);
}
Random random = new Random();
for (Node node : nodes) {
for (int edgeIndex = 0; edgeIndex < edgesPerNode; edgeIndex++) {
int randomTargetNodeIndex = random.nextInt(nodes.size());
Node targetNode = nodes.get(randomTargetNodeIndex);
node.getNeighbors().add(targetNode);
}
}
return graph;
}
}
The Stack Trace of the StackOverflowError repeatedly contains the following sequence (directly one after the other):
Code:
at org.hibernate.engine.spi.CascadingActions$6.cascade(CascadingActions.java:277) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:350) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:293) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:161) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:118) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.event.internal.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:432) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.event.internal.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:248) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:317) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:186) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:886) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]
at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:868) ~[hibernate-core-4.3.7.Final.jar:4.3.7.Final]