JPA/Hibernate 无法理解持久化顺序
JPA/Hibernate cannot understand persist order
我正在尝试了解 jpa/hibernate "magic" 在实践中是如何工作的,以避免将来(和常见的)陷阱。
所以我创建了一些简单的 JUnit 测试,其中指令集完全相同,但 em.persist()
的调用顺序不同。
请注意,我使用的是 Hibernate 5.2.10 和 bean 验证器 5.2.4 hibernate.jdbc.batch_size 和 hibernate.order_inserts(有关 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,遵循规范指令顺序,显然通过了。
test2,它在构造函数调用后立即调用 persist
,在提交时因 database 空约束违规而失败EDGE.ENDNODE_ID
.
我认为这不应该发生,我也相信:
- 应该在持久化时抛出异常,而不是在提交时抛出
- 应该也不例外,因为在提交时,
e1
应该与 n1
和 n2
链接。
test3,调用 persist
延迟,直接在 em.persist(n1);
行失败(而不是提交)。
我觉得这也不应该发生。
e1.endNode
引用瞬态实体时(通过级联)抛出异常,而在 test2 中,即使 e1.endNode
为 NULL,也不会在 persist 上调用异常。
有人可以解释一下 为什么 test2 异常在提交时抛出而 test3 在持久化时抛出(使用 order_inserts) ?
在提交之前,Hibernate 不应该缓存(和排序)插入语句吗?
更新
我不需要 修复,我需要解释。我会尽量让问题更清楚:
- T2: 为什么 hibernate 忽略对持久化的@NotNull 约束?
- T2:为什么,尽管发出了
e1.setEndNode(n2)
,但空值到达了数据库?在调用 persist 和 track end-node n2
后,不应该管理 e1
?
- T3:为什么 hibernate 会提前抛出 TPVE(在 persist 而不是 flush/commit 上)?不应该休眠等到刷新时间抛出异常吗?这不是与 T2 中的行为形成对比吗?顺便说一句,persist 的 javadoc 没有指定 TPVE。
我会尝试自己回答:
- hibernate 尝试尽可能晚地推迟验证(我完全没问题)。
- 我找不到任何合理的解释...这对我来说根本没有意义。
持久化后,被管理的n1
与瞬态的e1
有关系,必须避免这种情况。
不过我可以:
Node n1 = new Node("n31");
em.persist(n1);
Edge e1 = new Edge();
e1.setEndNode(n1);
// same situation on this line
得到确切的情况(managed n1
与transient e1
有关),所以一定是另有原因。
长话短说,我需要了解这种明显有争议的行为的原因,并确定它们是否是故意的(也许是错误?)。
谢谢@AlanHay,现在更清楚了。
我想你是对的,似乎 hibernate 在 persist 上生成插入语句。现在顺序有意义了。
尽管如此,我仍然认为这是一个有争议且愚蠢的实现。
你到底为什么要在 persist 上生成插入语句?
一个聪明的 impl 应该记住托管实体并在 flush/commit 之前生成插入语句,生成最新的语句。
为什么你在生成语句时不 运行 bean 验证器?
可用,但未使用。
关于order_inserts的一句话:它用于按table分组插入,即:
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)
它不仅可以用作优化,还可以控制语句顺序(第一个块失败,但第二个块成功)。
反正在这种情况下,是无关紧要的。
T2: em.persist(entity);
http://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#persist(java.lang.Object)
Make an instance managed and persistent.
没有说明何时将数据刷新到数据库。如果没有明确的刷新语句,那么这将在持久性提供者决定时发生:哪个(在同一事务中没有发出任何查询,其结果可能会受到未决更改的影响)最有可能是事务提交。
http://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#flush()
Synchronize the persistence context to the underlying database.
因此,您可以通过调用 em.persist()
然后调用 em.flush()
或通过向 Edges 发出查询来使 T2 在提交之前失败:在后一种情况下,挂起的更改将被自动刷新以确保查询返回一致的结果。
@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);
//explict flush : should fail immediately
//em.flush();
//implicit flush : should fail immediately
//Query query = em.createQUery("select e from Edge e");
//query.getResultList();
e1.setStartNode(n1);
n1.getExitEdges().add(e1);
e1.setEndNode(n2);
n2.getExitEdges().add(e1);
});
}
T3: em.persist(n1);
这里我们可以看出这是一个Hibernate异常而不是SQL异常。在调用 persist 时,Hibernate 知道 n1 引用了瞬态实例 e1。您需要使 e1 持久化或向关系添加 @Cascade
选项。
进一步查看 JPA 规范:
http://download.oracle.com/otndocs/jcp/persistence-2_1-fr-eval-spec/index.html
3.2.4 同步到数据库
更新
您似乎认为使用 API 所看到的结果是 "apparently controversial" 行为,并且 order_inserts 应该以某种方式修复损坏的代码。
据我所知,Order inserts 是一种优化 SQL 语句编写的方法,通过 与 API 的正确交互生成有效 in -内存模型:不修复 API 的错误使用。
如果我们假设 Hibernate 在调用 persist()
时生成缓冲的 SQL 语句(毕竟它会在其他地方执行此操作),那么该行为是完全合理的。此时它无法为空关系设置值。然而,似乎在添加您期望的关系之后(可能是由于 order_inserts 的存在,或者可能与此无关)它会足够聪明地返回并修改已经生成的 SQL 插入语句.
T2 > em.persist(e1);
> 生成一个插入语句,其中 endnode_id 为 null。
T3 > em.persist(n1);
> n1 与瞬态 endNode n2 有关系。我用它做什么?没有cacade所以我不能保存所以抛出异常
我把问题集中在一个最小的例子上,它确实是 bug。
考虑一个具有两个属性的简单实体 节点:
- name(required with a @NotNull and the db column is not allowing nulls)
- 标签(可选并且数据库列允许空值)
然后考虑这个测试:
@Test
public void test1()
{
accept(em ->
{
Node n = new Node();
em.persist(n);
n.setName("node-1");
n.setLabel("label-1");
});
}
test1 将失败:
Caused by: java.sql.SQLIntegrityConstraintViolationException: Column 'NAME' cannot accept a NULL value.
不连贯是因为没有满足一致的行为。
一致的行为是以下之一:
- a
javax.validation.ConstraintViolationException
(对于 @NotNull
)应该被抛出(在坚持或 flush/commit 上)
- 或者 test1 应该通过
假设预期行为是抛出验证异常,验证器在 flush/commit 时间在实体上执行,但那时实体已设置 "name"。
然后,这会导致正在验证的实体与要执行的生成语句之间不同步,从而使验证 return 成为误报。
为了展示它,考虑第二个简单的测试:
@Test
public void test2()
{
accept(em ->
{
Node n = new Node();
em.persist(n);
});
}
正确,这是失败的:
Caused by: javax.validation.ConstraintViolationException: Validation failed for classes [hibernate.model.Node] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='may not be null', propertyPath=name, rootBeanClass=class hibernate.model.Node, messageTemplate='{javax.validation.constraints.NotNull.message}'}
]
另一方面,假设预期行为是 test1 应该通过,那么不连贯是由于语句生成时间。
为了展示它,考虑第二个简单的测试:
@Test
public void test3()
{
accept(em ->
{
Node n = new Node();
n.setName("node-3");
em.persist(n);
n.setLabel("label-3");
});
Node n = apply(em -> em.createQuery("select x from Node x", Node.class).getSingleResult());
Assert.assertEquals("label-3", n.getLabel());
}
即使测试通过,也会生成(并执行)两个语句。
Hibernate: insert into Node (label, name, id) values (?, ?, ?)
Hibernate: update Node set label=?, name=? where id=?
我想第一条语句是在 persist 上生成的,第二条语句是在 flush/commit 上生成的;
但是,在这种情况下,我期望在 实体已验证后 立即生成单个插入语句(然后在 flush/commit 时间)。
总之,我看到两种可能的解决方案:
- 运行 persist()
中的验证器
- 将报表生成推迟到 flush/commit 时间
我正在尝试了解 jpa/hibernate "magic" 在实践中是如何工作的,以避免将来(和常见的)陷阱。
所以我创建了一些简单的 JUnit 测试,其中指令集完全相同,但 em.persist()
的调用顺序不同。
请注意,我使用的是 Hibernate 5.2.10 和 bean 验证器 5.2.4 hibernate.jdbc.batch_size 和 hibernate.order_inserts(有关 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,遵循规范指令顺序,显然通过了。
test2,它在构造函数调用后立即调用 persist
,在提交时因 database 空约束违规而失败EDGE.ENDNODE_ID
.
我认为这不应该发生,我也相信:
- 应该在持久化时抛出异常,而不是在提交时抛出
- 应该也不例外,因为在提交时,
e1
应该与n1
和n2
链接。
test3,调用 persist
延迟,直接在 em.persist(n1);
行失败(而不是提交)。
我觉得这也不应该发生。
e1.endNode
引用瞬态实体时(通过级联)抛出异常,而在 test2 中,即使 e1.endNode
为 NULL,也不会在 persist 上调用异常。
有人可以解释一下 为什么 test2 异常在提交时抛出而 test3 在持久化时抛出(使用 order_inserts) ?
在提交之前,Hibernate 不应该缓存(和排序)插入语句吗?
更新
我不需要 修复,我需要解释。我会尽量让问题更清楚:
- T2: 为什么 hibernate 忽略对持久化的@NotNull 约束?
- T2:为什么,尽管发出了
e1.setEndNode(n2)
,但空值到达了数据库?在调用 persist 和 track end-noden2
后,不应该管理e1
? - T3:为什么 hibernate 会提前抛出 TPVE(在 persist 而不是 flush/commit 上)?不应该休眠等到刷新时间抛出异常吗?这不是与 T2 中的行为形成对比吗?顺便说一句,persist 的 javadoc 没有指定 TPVE。
我会尝试自己回答:
- hibernate 尝试尽可能晚地推迟验证(我完全没问题)。
- 我找不到任何合理的解释...这对我来说根本没有意义。
持久化后,被管理的
n1
与瞬态的e1
有关系,必须避免这种情况。
不过我可以:Node n1 = new Node("n31"); em.persist(n1); Edge e1 = new Edge(); e1.setEndNode(n1); // same situation on this line
得到确切的情况(managed n1
与transient e1
有关),所以一定是另有原因。
长话短说,我需要了解这种明显有争议的行为的原因,并确定它们是否是故意的(也许是错误?)。
谢谢@AlanHay,现在更清楚了。
我想你是对的,似乎 hibernate 在 persist 上生成插入语句。现在顺序有意义了。
尽管如此,我仍然认为这是一个有争议且愚蠢的实现。
你到底为什么要在 persist 上生成插入语句?
一个聪明的 impl 应该记住托管实体并在 flush/commit 之前生成插入语句,生成最新的语句。
为什么你在生成语句时不 运行 bean 验证器?
可用,但未使用。
关于order_inserts的一句话:它用于按table分组插入,即:
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)
它不仅可以用作优化,还可以控制语句顺序(第一个块失败,但第二个块成功)。
反正在这种情况下,是无关紧要的。
T2: em.persist(entity);
http://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#persist(java.lang.Object)
Make an instance managed and persistent.
没有说明何时将数据刷新到数据库。如果没有明确的刷新语句,那么这将在持久性提供者决定时发生:哪个(在同一事务中没有发出任何查询,其结果可能会受到未决更改的影响)最有可能是事务提交。
http://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#flush()
Synchronize the persistence context to the underlying database.
因此,您可以通过调用 em.persist()
然后调用 em.flush()
或通过向 Edges 发出查询来使 T2 在提交之前失败:在后一种情况下,挂起的更改将被自动刷新以确保查询返回一致的结果。
@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);
//explict flush : should fail immediately
//em.flush();
//implicit flush : should fail immediately
//Query query = em.createQUery("select e from Edge e");
//query.getResultList();
e1.setStartNode(n1);
n1.getExitEdges().add(e1);
e1.setEndNode(n2);
n2.getExitEdges().add(e1);
});
}
T3: em.persist(n1);
这里我们可以看出这是一个Hibernate异常而不是SQL异常。在调用 persist 时,Hibernate 知道 n1 引用了瞬态实例 e1。您需要使 e1 持久化或向关系添加 @Cascade
选项。
进一步查看 JPA 规范:
http://download.oracle.com/otndocs/jcp/persistence-2_1-fr-eval-spec/index.html
3.2.4 同步到数据库
更新
您似乎认为使用 API 所看到的结果是 "apparently controversial" 行为,并且 order_inserts 应该以某种方式修复损坏的代码。
据我所知,Order inserts 是一种优化 SQL 语句编写的方法,通过 与 API 的正确交互生成有效 in -内存模型:不修复 API 的错误使用。
如果我们假设 Hibernate 在调用 persist()
时生成缓冲的 SQL 语句(毕竟它会在其他地方执行此操作),那么该行为是完全合理的。此时它无法为空关系设置值。然而,似乎在添加您期望的关系之后(可能是由于 order_inserts 的存在,或者可能与此无关)它会足够聪明地返回并修改已经生成的 SQL 插入语句.
T2 >
em.persist(e1);
> 生成一个插入语句,其中 endnode_id 为 null。T3 >
em.persist(n1);
> n1 与瞬态 endNode n2 有关系。我用它做什么?没有cacade所以我不能保存所以抛出异常
我把问题集中在一个最小的例子上,它确实是 bug。
考虑一个具有两个属性的简单实体 节点:
- name(required with a @NotNull and the db column is not allowing nulls)
- 标签(可选并且数据库列允许空值)
然后考虑这个测试:
@Test
public void test1()
{
accept(em ->
{
Node n = new Node();
em.persist(n);
n.setName("node-1");
n.setLabel("label-1");
});
}
test1 将失败:
Caused by: java.sql.SQLIntegrityConstraintViolationException: Column 'NAME' cannot accept a NULL value.
不连贯是因为没有满足一致的行为。 一致的行为是以下之一:
- a
javax.validation.ConstraintViolationException
(对于@NotNull
)应该被抛出(在坚持或 flush/commit 上) - 或者 test1 应该通过
假设预期行为是抛出验证异常,验证器在 flush/commit 时间在实体上执行,但那时实体已设置 "name"。
然后,这会导致正在验证的实体与要执行的生成语句之间不同步,从而使验证 return 成为误报。
为了展示它,考虑第二个简单的测试:
@Test
public void test2()
{
accept(em ->
{
Node n = new Node();
em.persist(n);
});
}
正确,这是失败的:
Caused by: javax.validation.ConstraintViolationException: Validation failed for classes [hibernate.model.Node] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='may not be null', propertyPath=name, rootBeanClass=class hibernate.model.Node, messageTemplate='{javax.validation.constraints.NotNull.message}'}
]
另一方面,假设预期行为是 test1 应该通过,那么不连贯是由于语句生成时间。
为了展示它,考虑第二个简单的测试:
@Test
public void test3()
{
accept(em ->
{
Node n = new Node();
n.setName("node-3");
em.persist(n);
n.setLabel("label-3");
});
Node n = apply(em -> em.createQuery("select x from Node x", Node.class).getSingleResult());
Assert.assertEquals("label-3", n.getLabel());
}
即使测试通过,也会生成(并执行)两个语句。
Hibernate: insert into Node (label, name, id) values (?, ?, ?)
Hibernate: update Node set label=?, name=? where id=?
我想第一条语句是在 persist 上生成的,第二条语句是在 flush/commit 上生成的; 但是,在这种情况下,我期望在 实体已验证后 立即生成单个插入语句(然后在 flush/commit 时间)。
总之,我看到两种可能的解决方案:
- 运行 persist() 中的验证器
- 将报表生成推迟到 flush/commit 时间