【发布时间】:2017-09-21 16:06:27
【问题描述】:
我试图了解 jpa/hibernate“魔法”真正在实践中是如何工作的,以避免未来(和常见)的陷阱。
所以我创建了一些简单的JUnit测试,其中指令集完全相同,但em.persist()的调用顺序不同。
请注意,我正在使用带有 hibernate.jdbc.batch_size 和 hibernate.order_inserts 的 Hibernate 5.2.10 和 bean 验证器 5.2.4(有关 persistence.xml 的更多详细信息)。
您也可以在GitHub上查看完整代码
两个测试实体:
@Entity
public class Node implements Serializable
{
@Id
private long id = System.nanoTime();
@NotNull
@Column(nullable = false)
private String name;
@OneToMany(mappedBy = "startNode", cascade = ALL, orphanRemoval = true)
private Set<Edge> exitEdges = new HashSet<>();
@OneToMany(mappedBy = "endNode", cascade = ALL, orphanRemoval = true)
private Set<Edge> enterEdges = new HashSet<>();
public Node() {}
public Node(String name)
{
this.name = name;
}
...
}
和
@Entity
public class Edge implements Serializable
{
@Id
private long id = System.nanoTime();
@NotNull
@ManyToOne
private Node startNode;
@NotNull
@ManyToOne
private Node endNode;
...
}
测试:
@Test
public void test1()
{
accept(em ->
{
Node n1 = new Node("n11");
em.persist(n1);
Node n2 = new Node("n12");
em.persist(n2);
Edge e1 = new Edge();
e1.setStartNode(n1);
n1.getExitEdges().add(e1);
e1.setEndNode(n2);
n2.getExitEdges().add(e1);
em.persist(e1);
});
}
@Test
public void test2()
{
accept(em ->
{
Node n1 = new Node("n21");
em.persist(n1);
Node n2 = new Node("n22");
em.persist(n2);
Edge e1 = new Edge();
em.persist(e1); // <-------- early persist call (no exception)
e1.setStartNode(n1);
n1.getExitEdges().add(e1);
e1.setEndNode(n2);
n2.getExitEdges().add(e1);
});
// exception here: java.sql.SQLIntegrityConstraintViolationException: Column 'ENDNODE_ID' cannot accept a NULL value.
}
@Test
public void test3()
{
accept(em ->
{
Node n1 = new Node("n31");
Node n2 = new Node("n32");
Edge e1 = new Edge();
e1.setStartNode(n1);
n1.getExitEdges().add(e1);
e1.setEndNode(n2);
n2.getExitEdges().add(e1);
em.persist(n1); // <-------- late persist calls: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved beforeQuery current operation : hibernate.model.Edge.endNode -> hibernate.model.Node
em.persist(n2);
em.persist(e1);
});
}
test1,遵循canonical指令顺序,显然通过了。
test2,在构造函数调用后立即调用 persist,在提交时失败,EDGE.ENDNODE_ID 违反 database 空约束。
我认为这不应该发生,并且我认为:
- 应该在持久化时抛出异常,而不是在提交时抛出
- 应该没有例外,因为在提交时,
e1应该与n1和n2链接。
test3,延迟调用 persist,直接在 em.persist(n1); 行失败(而不是在提交时)。
我认为这也不应该发生。e1.endNode 引用瞬态实体时(通过级联)引发异常,而在 test2 中,即使 e1.endNode 为 NULL,也不会在持久性上调用异常。
谁能解释一下为什么 test2 异常在提交时抛出,而 test3 在持久时抛出(使用 order_inserts 时)?
Hibernate 不应该在提交之前缓存(和排序)插入语句吗?
更新
我不需要修复,我需要一个解释。我会尽量让问题更清楚:
- T2:为什么 hibernate 忽略了坚持的@NotNull 约束?
- T2:为什么,虽然发出了
e1.setEndNode(n2),但一个空值到达了数据库?在调用persist和track end-noden2之后不应该管理e1吗? - T3:为什么 hibernate 会提前抛出 TPVE(持续而不是刷新/提交)?休眠不应该等到刷新时间才抛出异常吗?这与T2中的行为不形成对比吗?顺便说一句,persist 的 javadoc 没有指定 TPVE。
我会尽量回答自己:
- hibernate 尝试尽可能晚地推迟验证(对我来说完全没问题)。
- 我找不到任何合理的解释......这对我来说毫无意义。
-
persist 后,托管
n1将与瞬态e1有关系,必须避免这种情况。
不过我可以:Node n1 = new Node("n31"); em.persist(n1); Edge e1 = new Edge(); e1.setEndNode(n1); // same situation on this line
要获得确切的情况(托管n1与瞬态e1有关),所以肯定还有其他原因。
长话短说,我需要了解这种明显有争议的行为的原因,并确定它们是否是故意的(可能是错误?)。
谢谢@AlanHay,现在更清楚了。
我想你是对的,似乎hibernate在persist上生成插入语句。现在这个顺序是有意义的。
尽管如此,我仍然认为这是有争议且愚蠢的实现。
到底为什么要在persist上生成插入语句?
智能 impl 应该记住托管实体并在刷新/提交之前生成插入语句,从而生成最新的语句。
为什么在生成语句时不运行 bean 验证器?
它可用,但尚未使用。
关于order_inserts的一句话:用于按表对插入进行分组,即:
insert into Node (id, name) values (1, 'x')
insert into Edge (id, startnode_id, endnode_id) values (2, 1, 3)
insert into Node (id, name) values (3, 'y')
变成
insert into Node (id, name) values (1, 'x'), (3, 'y')
insert into Edge (id, startnode_id, endnode_id) values (2, 1, 3)
它不仅可以用作优化,还可以控制语句顺序(第一个块失败,但第二个成功)。
无论如何,在这种情况下,它是无关紧要的。
【问题讨论】:
标签: java hibernate jpa hibernate-5.x