【问题标题】:JPA/Hibernate cannot understand persist orderJPA/Hibernate 无法理解持久化顺序
【发布时间】:2017-09-21 16:06:27
【问题描述】:

我试图了解 jpa/hibernate“魔法”真正在实践中是如何工作的,以避免未来(和常见)的陷阱。

所以我创建了一些简单的JUnit测试,其中指令集完全相同,但em.persist()的调用顺序不同。

请注意,我正在使用带有 hibernate.jdbc.batch_sizehibernate.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 应该与 n1n2 链接。

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


谁能解释一下为什么 test2 异常在提交时抛出,而 test3 在持久时抛出(使用 order_inserts 时)?

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


更新

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

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

我会尽量回答自己:

  1. hibernate 尝试尽可能晚地推迟验证(对我来说完全没问题)。
  2. 我找不到任何合理的解释......这对我来说毫无意义。
  3. 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


    【解决方案1】:

    T2: em.persist(entity);

    http://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#persist(java.lang.Object)

    使实例受管理和持久化。

    没有说明何时将数据刷新到数据库。在没有显式刷新语句的情况下,这将在持久性提供者决定时发生:哪个(在没有在同一事务中发出任何查询的情况下,其结果可能会受到挂起更改的影响)最有可能是当事务提交。

    http://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#flush()

    将持久化上下文同步到底层数据库。

    因此,您可以通过调用 em.persist() 然后调用 em.flush() 或通过发出查询来使 T2 在提交之前失败 Edges:在后一种情况下,挂起的更改将被自动刷新以确保查询返回一致的结果.

    @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 所看到的结果是“明显有争议”的行为,并且 order_inserts 应该以某种方式修复您损坏的代码。

    据我所知,订单插入是一种优化 SQL 语句编写的方法,该 SQL 语句通过 与 API 的正确交互以实现有效的内存模型:不修复不正确的API的使用。

    如果我们假设 Hibernate 在调用 persist() 时生成缓冲的 SQL 语句(毕竟它会在哪里这样做),那么这种行为是完全合理的。此时,它无法为空关系设置值。然而,似乎在添加您期望的关系之后(可能是由于 order_inserts 的存在或者可能不管这一点)它会足够聪明地返回并修改已经生成的 SQL 插入语句。

    • T2 > em.persist(e1); > 生成一个 endnode_id 为 null 的插入语句。

    • T3 > em.persist(n1); > n1 与瞬态 endNode n2 有关系。我该怎么办?没有级联,所以我无法保存它所以抛出异常。

    【讨论】:

    • 谢谢,但这不是我要找的。请查看更新。
    • 查看更新。您看到的行为是完全合乎逻辑的。如果您忘记 order_inserts 因为它没有任何意义,那么希望这很清楚。
    • 谢谢,我想你是对的,现在更清楚了。尽管如此,我仍然认为这是有争议且愚蠢的实现。
    【解决方案2】:

    我将问题集中在一个最小的例子上,确实是bug

    考虑一个具有两个属性的简单实体节点

    • 名称(必需,带有 @NotNull 且 db 列不允许允许空值)
    • 标签(可选并且 db 列允许空值)

    然后考虑这个测试:

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

    不连贯性在于没有满足一致的行为。 一致的行为是其中之一:

    • 应该抛出一个javax.validation.ConstraintViolationException(对于@NotNull)(在持久化或刷新/提交时)
    • 或者 test1 应该通过

    假设预期的行为是一个被抛出的验证异常,验证器在刷新/提交时间在实体上执行,但此时实体已设置“名称”。
    然后,这会导致正在验证的实体和要执行的生成语句之间不同步,从而使验证返回误报。

    为了展示它,考虑第二个简单的测试:

    @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()中运行验证器
    • 将语句生成推迟到刷新/提交时间

    【讨论】: