我正在尝试使用 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
。
我如何在数据库中存储10.000个以上节点的新图表,因为Hibernate似乎已经在1000个节点的图表上扼住了StackOverflowError
?任何有用的技巧?
如何在不执行N + 1 SQL查询的情况下完全加载图表并解析所有延迟引用?
我不知道如何解决问题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 会产生相同的结果。
编辑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]
答案 0 :(得分:8)
在过去的24小时内,我对这个主题进行了大量的网络研究,我试着在这里给出一个试验性的答案。如果我错了,请纠正我。
这似乎是ORM的一般问题。从本质上讲,&#34;合并&#34;算法是递归的。如果模型中有一个路径(从实体到实体),其中包含太多实体,而没有引用其间的已知实体,则算法的递归深度大于JVM的堆栈大小。
如果您知道您的模型对于JVM的堆栈大小而言稍微过大,则可以通过使用start参数 -Xss (以及合适的值)来增加该值以增加它。但请注意,此值是静态的,因此如果您加载的模型比以前更大,则必须再次增加它。
这绝对不是对象关系映射精神的解决方案,但根据我目前的知识,它是唯一能够有效扩展模型尺寸的解决方案。我们的想法是,使用包含目标实体的@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...
}
我实际上是被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)
几年前,我遇到了与您相同的问题,除了您的帖子和答案,找不到其他东西了。
现在,我想在您的回答中添加一些内容,希望对您有所帮助。
设置 -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>
使用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,不需要增加堆栈大小,并且测试对我来说快了几倍。