title: How to save and load a large Graph structure with JPA and Hibernate?
body: <p>I'm trying to persist and load the following simple structure (resembling a directed graph) using <strong>JPA 2.1</strong>, <strong>Hibernate 4.3.7</strong> and <strong>Spring Data</strong>:</p>
  
  <p><strong>Graph.java</strong></p>
  
  <pre><code>@Entity
  public class Graph extends PersistableObject {
  
      @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "graph")
      private Set&lt;Node&gt; nodes = new HashSet&lt;Node&gt;();
  
      // getters, setters...
  }
  </code></pre>
  
  <p><strong>Node.java</strong></p>
  
  <pre><code>@Entity
  public class Node extends PersistableObject {
  
      @ManyToMany(fetch = FetchType.LAZY, cascade = { CascadeType.MERGE, CascadeType.PERSIST })
      private Set&lt;Node&gt; neighbors = new HashSet&lt;Node&gt;();
  
      @ManyToOne(fetch = FetchType.EAGER, cascade = { CascadeType.MERGE })
      private Graph graph;
  
      // getters, setters...
  }
  </code></pre>
  
  <h1>The Problem</h1>
  
  <p>In most cases, the lazy loading behaviour is fine. The problem is that, on some occasions in my application, I need to fully load a given graph (including all lazy references) and also persist a full graph in an <strong>efficient</strong> way, <strong>without</strong> performing <strong>N+1 SQL queries</strong>. Also, when <strong>storing</strong> a new graph, I get a <code>StackOverflowError</code> as soon as the graph becomes too big (> 1000 nodes).</p>
  
  <h1>Questions</h1>
  
  <ol>
  <li><p>How can I store a new graph in the database with 10.000+ nodes, given that Hibernate seems to choke on a graph with 1000 nodes with a <code>StackOverflowError</code> already? Any useful tricks?</p></li>
  <li><p>How can I fully load a graph and resolve all lazy references without performing N+1 SQL queries?</p></li>
  </ol>
  
  <h1>What I tried so far</h1>
  
  <p>I have no clue how to solve problem 1). As for problem 2), I tried to use the following HQL query:</p>
  
  <p>I'm currently trying to do it using HQL with fetch joins:</p>
  
  <pre><code>FROM Graph g LEFT JOIN FETCH g.nodes node LEFT JOIN FETCH node.neighbors WHERE g.id = ?1
  </code></pre>
  
  <p>... where ?1 refers to a string parameter containing the graph id. However, this seems to result in one SQL SELECT per node stored in the graph, which leads to horrible performance on graphs with several thousands of nodes. Using Hibernate's <strong>FetchProfiles</strong> produced the same result.</p>
  
  <h1>Important -EDIT-</h1>
  
  <p><strong>EDIT 1:</strong> It turns out that <strong>Spring Data JpaRepositories</strong> perform their <code>save(T)</code> operation by first calling <code>entityManager.merge(...)</code>, then calling <code>entityManager.persist(...</code>). The <code>StackOverflowError</code> does <strong>not</strong> occur on a "raw" <code>entityManager.persist(...)</code>, but it <strong>does</strong> occur in <code>entityManager.merge(...)</code>. It still doesn't solve the issue though, why does this happen on a merge?</p>
  
  <p><strong>EDIT 2:</strong> I think that this is really a bug in Hibernate. I've filed a bug report with a complete, self-contained JUnit test project. In case somebody is interested, you can find it here:
  <a href="https://hibernate.atlassian.net/browse/HHH-9565" rel="noreferrer">Hibernate JIRA</a></p>
  
  <h1>Supplementary Material</h1>
  
  <p>Here's the <code>PersistableObject</code> class which uses a UUID for it's <code>@ID</code>, and an eclipse-generated <code>hashCode()</code> and <code>equals(...)</code> method based on that ID.</p>
  
  <p><strong>PersistableObject.java</strong></p>
  
  <pre><code>@MappedSuperclass
  public abstract class PersistableObject {
  
      @Id
      private String id = UUID.randomUUID().toString();
  
      // hashCode() and equals() auto-generated by eclipse based on this.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;
      }
  
      // getters, setters...
  
  }
  </code></pre>
  
  <p>If you want to try it for yourself, here's a factory that generates a random graph:</p>
  
  <p><strong>GraphFactory.java</strong></p>
  
  <pre><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&lt;Node&gt; nodes = new ArrayList&lt;Node&gt;();
          for (int nodeIndex = 0; nodeIndex &lt; 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 &lt; edgesPerNode; edgeIndex++) {
                  int randomTargetNodeIndex = random.nextInt(nodes.size());
                  Node targetNode = nodes.get(randomTargetNodeIndex);
                  node.getNeighbors().add(targetNode);
              }
          }
          return graph;
      }
  }
  </code></pre>
  
  <p><strong>The Stack Trace</strong></p>
  
  <p>The stack trace of the <code>StackOverflowError</code> repeatedly contains the following sequence (directly one after the other):</p>
  
  <pre><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]
  </code></pre>