Pregunta ¿Cómo guardar y cargar una gran estructura de gráficos con JPA e Hibernate?


Intento persistir y cargar la siguiente estructura simple (parecida a un gráfico dirigido) utilizando JPA 2.1, Hibernate 4.3.7 y Datos de primavera:

Graph.java

@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.java

@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...
}

El problema

En la mayoría de los casos, el comportamiento de carga diferida está bien. El problema es que, en algunas ocasiones en mi aplicación, necesito cargar completamente un gráfico dado (incluidas todas las referencias flojas) y también persistir un gráfico completo en una eficiente camino, sin amaestrado N + 1 consultas SQL. Además, cuando almacenamiento un nuevo gráfico, obtengo un StackOverflowError tan pronto como el gráfico se vuelva demasiado grande (> 1000 nodos).

Preguntas

  1. ¿Cómo puedo almacenar un nuevo gráfico en la base de datos con más de 10.000 nodos, dado que Hibernate parece ahogarse en un gráfico con 1000 nodos con un StackOverflowError ¿ya? ¿Algún trucos útiles?

  2. ¿Cómo puedo cargar completamente un gráfico y resolver todas las referencias perezosas sin realizar N + 1 consultas SQL?

Lo que intenté hasta ahora

No tengo ni idea de cómo resolver el problema 1). En cuanto al problema 2), traté de usar la siguiente consulta HQL:

Actualmente estoy tratando de hacerlo usando HQL con fetch joins:

FROM Graph g LEFT JOIN FETCH g.nodes node LEFT JOIN FETCH node.neighbors WHERE g.id = ?1

... donde? 1 se refiere a un parámetro de cadena que contiene la identificación del gráfico. Sin embargo, esto parece dar como resultado un SQL SELECT por nodo almacenado en el gráfico, lo que conduce a un rendimiento horrible en los gráficos con varios miles de nodos. Usando Hibernate's FetchProfiles produjo el mismo resultado.

Importante -EDIT-

EDIT 1: Resulta que Spring Data JpaRepositories realizar su save(T) operación llamando primero entityManager.merge(...), luego llamando entityManager.persist(...) los StackOverflowError hace no ocurrir en un "crudo" entityManager.persist(...), pero hace ocurre en entityManager.merge(...). Todavía no resuelve el problema, ¿por qué sucede esto en una fusión?

EDICION 2: Creo que esto es realmente un error en Hibernate. He archivado un informe de error con un proyecto de prueba de JUnit completo e independiente. En caso de que alguien esté interesado, puede encontrarlo aquí: Hibernate JIRA

Material suplementario

Aquí esta la PersistableObject clase que usa un UUID para su @IDy un eclipse generado hashCode() y equals(...) método basado en esa ID.

PersistableObject.java

@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...

}

Si quieres probarlo por ti mismo, aquí hay una fábrica que genera un gráfico aleatorio:

GraphFactory.java

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;
    }
}

El rastro de la pila

El rastro de la pila del StackOverflowError contiene repetidamente la siguiente secuencia (directamente una después de la otra):

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]

8
2018-01-12 12:48


origen


Respuestas:


Durante las últimas 24 horas investigué mucho sobre este tema e intentaré dar una respuesta tentativa aquí. Por favor, corrígeme si me equivoco en algo.

Problema: Hibernate StackOverflowException en entityManager.merge (...)

Esto parece ser un problema general con ORM. Por naturaleza, el algoritmo "merge" es recursivo. Si hay una ruta (de entidad a entidad) en su modelo que tiene demasiadas entidades en ella, sin referirse nunca a una entidad conocida entre ellas, la profundidad de recursión del algoritmo es mayor que el tamaño de la pila de su JVM.

Solución 1: aumente el tamaño de la pila de su JVM

Si sabe que su modelo es demasiado grande para el tamaño de pila de su JVM, puede aumentar ese valor utilizando el parámetro de inicio -Xss (y un valor adecuado) para aumentarlo. Sin embargo, tenga en cuenta que este valor es estático, por lo que si carga un modelo más grande que antes, tendrá que aumentarlo de nuevo.

Solución 2: rompiendo las cadenas de entidades

Definitivamente, esta no es una solución en el espíritu del Mapeo Relacional de Objetos, pero para mi conocimiento actual, es la única solución que se escala efectivamente con el crecimiento del tamaño del modelo. La idea es que reemplace una referencia Java normal en su @Entity clases con un valor primitivo que contiene el @Id valor de la entidad objetivo en su lugar. Entonces, si tu objetivo @Entity utiliza un valor de id de tipo long, tendrías que almacenar un long valor. Es entonces hasta la capa de aplicación para resolver la referencia según sea necesario (mediante la realización de un findById(...) consulta en la base de datos).

Aplicado al escenario del gráfico desde la publicación de la pregunta, tendríamos que cambiar el Node clase a esto:

@Entity
public class Node extends PersistableObject {

    // note this new mapping!
    @ElementCollection(fetch = FetchType.EAGER)
    private Set<String> neighbors = new HashSet<String>();

    @ManyToOne(fetch = FetchType.LAZY, cascade = { CascadeType.MERGE })
    private Graph graph;

    // getters, setters...

}

Problema: N + 1 SQL selecciona

En realidad, fui engañado por Spring e Hibernate aquí. Mi prueba de unidad utilizó un JpaRepository y llamado repository.save(graph) seguido por repository.fullyLoadById(graphId) (que tenía una @Query anotación usando la consulta de unión de búsqueda de HQL desde la publicación de pregunta) y midió el tiempo para cada operación. Las consultas de selección de SQL que aparecieron en el registro de mi consola no venir de la fullyLoadById consulta, pero de repository.save(graph). Lo que hacen los depósitos de primavera aquí es llamar primero entityManager.merge(...) en el objeto que queremos guardar Merge, a su vez, recupera el estado actual de la entidad desde la base de datos. Esta obtención da como resultado la gran cantidad de sentencias SQL select que experimenté. Mi consulta de carga en realidad se realizó en una sola consulta SQL, como estaba previsto.

Solución:

Si tiene un gráfico de objetos bastante grande y sabe que es definitivamente nuevo, no está contenido en la base de datos y no hace referencia a ninguna entidad que esté almacenada en la base de datos, puede omitir el merge(...) paso y directamente llama entityManager.persist(...) en él para un mejor rendimiento. Los repositorios de Spring siempre usan merge(...) por razones de seguridad. persist(...) intentará un SQL INSERT declaración, que fallar si ya hay una fila con la identificación dada en la base de datos.

Además, tenga en cuenta que Hibernate siempre registrará todas las consultas una por una si usa hibernate.show_sql = true. El procesamiento por lotes JDBC tiene lugar después de que se generaron las consultas. Por lo tanto, si ve muchas consultas en su registro, esto no significa necesariamente que tenga tantas pasadas de ida y vuelta DB.


5
2018-01-13 08:47