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_sizehibernate.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 应该与 n1n2 链接。

test3,调用 persist 延迟,直接在 em.persist(n1); 行失败(而不是提交)。
我觉得这也不应该发生。
e1.endNode 引用瞬态实体时(通过级联)抛出异常,而在 test2 中,即使 e1.endNode 为 NULL,也不会在 persist 上调用异常。


有人可以解释一下 为什么 test2 异常在提交时抛出而 test3 在持久化时抛出(使用 order_inserts) ?

在提交之前,Hibernate 不应该缓存(和排序)插入语句吗?


更新

我不需要 修复,我需要解释。我会尽量让问题更清楚:

  1. T2: 为什么 hibernate 忽略对持久化的@NotNull 约束?
  2. T2:为什么,尽管发出了 e1.setEndNode(n2),但空值到达了数据库?在调用 persist 和 track end-node n2 后,不应该管理 e1?
  3. T3:为什么 hibernate 会提前抛出 TPVE(在 persist 而不是 flush/commit 上)?不应该休眠等到刷新时间抛出异常吗?这不是与 T2 中的行为形成对比吗?顺便说一句,persist 的 javadoc 没有指定 TPVE。

我会尝试自己回答:

  1. hibernate 尝试尽可能晚地推迟验证(我完全没问题)。
  2. 我找不到任何合理的解释...这对我来说根本没有意义。
  3. 持久化后,被管理的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 时间