如何使用JPA和Hibernate保存和加载大型Graph结构?

时间:2015-01-12 12:48:36

标签: hibernate jpa graph spring-data-jpa

我正在尝试使用 JPA 2.1 Hibernate 4.3.7 Spring Data <来坚持并加载以下简单结构(类似于有向图) /强>:

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

问题

在大多数情况下,延迟加载行为很好。问题是,在我的应用程序中的某些情况下,我需要完全加载给定的图形(包括所有惰性引用)并且还以高效方式保持完整图形,不用执行 N + 1个SQL查询。此外,当存储新图表时,只要图表变得太大(> 1000个节点),我就会得到StackOverflowError

问题

  1. 我如何在数据库中存储10.000个以上节点的新图表,因为Hibernate似乎已经在1000个节点的图表上扼住了StackOverflowError?任何有用的技巧?

  2. 如何在不执行N + 1 SQL查询的情况下完全加载图表并解析所有延迟引用?

  3. 到目前为止我尝试了什么

    我不知道如何解决问题1)。至于问题2),我尝试使用以下HQL查询:

    我目前正在尝试使用带有提取连接的HQL:

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

    ... where?1指的是包含图形ID的字符串参数。但是,这似乎导致图中存储了每个节点一个SQL SELECT,这导致具有数千个节点的图形上的可怕性能。使用Hibernate的 FetchProfiles 会产生相同的结果。

    重要的-EDIT -

    编辑1:事实证明 Spring Data JpaRepositories 首先调用save(T),然后调用entityManager.merge(...)来执行entityManager.persist(...操作})。 StackOverflowError在“原始”entityManager.persist(...)上发生,但entityManager.merge(...)中发生 。它仍然没有解决问题,为什么这会在合并时发生?

    编辑2:我认为这确实是Hibernate中的一个错误。我已经提交了一个错误报告,其中包含一个完整的,独立的JUnit测试项目。如果有人有兴趣,你可以在这里找到它: Hibernate JIRA

    补充材料

    这是使用UUID的PersistableObject@ID,以及基于该ID的eclipse生成的hashCode()equals(...)方法。

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

    如果您想亲自尝试一下,这是一个生成随机图的工厂:

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

    堆栈跟踪

    StackOverflowError的堆栈跟踪重复包含以下序列(直接一个接一个):

    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]
    

2 个答案:

答案 0 :(得分:8)

在过去的24小时内,我对这个主题进行了大量的网络研究,我试着在这里给出一个试验性的答案。如果我错了,请纠正我。

问题:在entityManager.merge(...)

上的Hibernate StackOverflowException

这似乎是ORM的一般问题。从本质上讲,&#34;合并&#34;算法是递归的。如果模型中有一个路径(从实体到实体),其中包含太多实体,而没有引用其间的已知实体,则算法的递归深度大于JVM的堆栈大小。

解决方案1:增加JVM的堆栈大小

如果您知道您的模型对于JVM的堆栈大小而言稍微过大,则可以通过使用start参数 -Xss (以及合适的值)来增加该值以增加它。但请注意,此值是静态的,因此如果您加载的模型比以前更大,则必须再次增加它。

解决方案2:打破实体链

这绝对不是对象关系映射精神的解决方案,但根据我目前的知识,它是唯一能够有效扩展模型尺寸的解决方案。我们的想法是,使用包含目标实体的@Entity值的原始值替换@Id类中的普通Java引用。因此,如果您的目标@Entity使用类型long的ID值,则必须存储long值。然后由应用程序层根据需要解析引用(通过对数据库执行findById(...)查询)。

从问题帖子应用于图表场景,我们必须将Node类更改为:

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

}

问题:N + 1 SQL选择

我实际上是被Spring和Hibernate愚弄了。我的单元测试使用JpaRepository并调用repository.save(graph)后跟repository.fullyLoadById(graphId)(使用问题帖子中的HQL提取连接查询进行@Query注释)并测量时间每次操作。在我的控制台日志中弹出的SQL select查询来自fullyLoadById查询,但来自repository.save(graph)。春天存储库在这里做的是首先在我们想要保存的对象上调用entityManager.merge(...)。反过来,合并从数据库中获取实体的当前状态。这个获取会导致我遇到的大量SQL select语句。我的加载查询实际上是按照预期在单个SQL查询中执行的。

解决方案:

如果您有一个相当大的对象图,并且您知道它确实是新的,未包含在数据库中,并且不引用存储在数据库中的任何实体,则可以跳过merge(...)步骤,直接在其上调用entityManager.persist(...)以获得更好的性能。出于安全原因,Spring存储库始终使用merge(...)persist(...)将尝试SQL INSERT语句,如果数据库中已存在具有给定ID的行,则失败

另外,请注意,如果您使用hibernate.show_sql = true,Hibernate将始终逐个记录所有查询。 JDBC批处理在生成查询后进行。因此,如果您在日志中看到大量查询,则并不一定意味着您有多次数据库往返。

答案 1 :(得分:0)

几年前,我遇到了与您相同的问题,除了您的帖子和答案,找不到其他东西了。

现在,我想在您的回答中添加一些内容,希望对您有所帮助。

增加JVM的堆栈大小

设置 -Xss 应该适用于大多数用例,但是,在这种情况下,这不是解决方案。 Each thread has at least one stack,其中一些具有更多功能(例如,取决于您使用的JVM,可能会有JVM OS线程大小和JVM本机线程大小)。

运行快速检查(使用VisualVM),我发现我的WildFly应用程序使用了150个以上的线程(!)。 Assuming, that the thread size is 1M(取决于您的VM和可能的许多其他因素),并将其增加到4(-Xss4M)could potentially quadruple all stacks,这将导致不使用150M,而是使用600M的内存,这很浪费。 / p>

不使用CASCADE

使用Hibernate 5.3.20,我测试了与您的示例非常相似的示例,发现从中删除了层叠

@ManyToMany(fetch = FetchType.LAZY, cascade = { CascadeType.MERGE, CascadeType.PERSIST })
private Set<Node> neighbors = new HashSet<Node>();

收件人

@ManyToMany(fetch = FetchType.LAZY)
private Set<Node> neighbours = new HashSet<Node>();

但是,您必须手动合并,保留等每个节点,与第二个解决方案中的相同,但这次保留“硬”链接。 对于想检查的人,我基于Hibernate测试用例模板创建了一个GitHub example

快速运行后,您会发现使用CASCASE的persistGraph()测试将在几秒钟后失败。为了看到绿色,增加堆栈-Xss6M。

第二个测试persistGraphNoCascade(),对nextNode(在您的情况下为邻居)不使用CASCADE,不需要增加堆栈大小,并且测试对我来说快了几倍。