簡體   English   中英

JPA / Hibernate無法理解持久順序

[英]JPA/Hibernate cannot understand persist order

我試圖了解jpa / hibernate的“魔術”在實際中是如何工作的,以避免將來(以及常見的)陷阱。

因此,我創建了一些簡單的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調用)在提交時失敗,並且違反了EDGE.ENDNODE_ID數據庫空約束。
我認為這不應該發生,並且我相信:

  • 應該在持久化而不是提交時拋出異常
  • 應該沒有例外,因為在提交時, e1應該同時與n1n2鏈接。

test3調用persist較晚,直接在em.persist(n1);上失敗em.persist(n1); 行(而不是提交)。
我認為這也不應該發生。
引用臨時實體的e1.endNode會拋出一個異常(級聯),而在test2中,即使e1.endNode為NULL也不會在persist上調用任何異常。


有人可以解釋為什么在提交時拋出test2異常,而在持久化時拋出test3(使用order_inserts時 )?

在提交之前,Hibernate是否不應該緩存(和順序)插入語句?


UPDATE

我不需要修復 ,我需要一個解釋。 我將嘗試使問題更清楚:

  1. T2: 為什么休眠狀態在持久化時忽略@NotNull約束?
  2. T2: 為什么 ,盡管發出了e1.setEndNode(n2) ,但空值到達了數據庫? 調用persist並跟蹤終端節點n2之后不應該管理e1嗎?
  3. T3: 為什么休眠模式會提前(在持久狀態而不是在刷新/提交狀態)拋出TPVE? 難道不應該在刷新時間之前休眠休眠以引發異常嗎? 這不是與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 

為了獲得確切的情況(受管理的n1與瞬態e1 ),因此必須有另一個原因。

簡而言之,我需要了解這種有爭議的行為的原因 ,並確定它們是否是故意的(可能是錯誤?)。


謝謝@AlanHay,現在更清楚了。
我想您是對的,似乎hibernate在persist上生成insert語句。 現在,順序變得有意義了。

盡管如此,我仍然認為這是有爭議的,而且是愚蠢的實現。

到底為什么會在persist上生成insert語句?
一個聰明的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)

它不僅可以用作優化,還可以用於控制語句順序(第一個塊失敗,但是第二個塊成功)。
無論如何,在這種情況下,這是無關緊要的。

T2: em.persist(entity);

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

使實例托管和持久化。

關於何時將數據刷新到數據庫什么也沒說。 在沒有顯式的flush語句的情況下,當持久性提供程序做出以下決定時,將發生以下情況:哪個(在同一事務中未發出任何查詢,其結果可能會受到掛起的更改的影響)將在以下情況下發生:事務提交。

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

將持久性上下文同步到基礎數據庫。

因此,可以通過調用em.persist()然后調用em.flush()或通過issung查詢Edge來使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異常。 在調用持久化時,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應該以某種方式修復損壞的代碼。

就我所知,訂單插入是一種針對有效內存模型優化通過與API的正確交互生成的SQL語句的編寫的方法:不修正API的不正確用法。

如果我們假設Hibernate在對persist()的調用上生成了緩沖的SQL語句persist()畢竟它將在其他地方執行此操作),那么該行為就很合理了。 此時,它無法為null關系設置值。 但是,在添加關系之后,您似乎希望(可能由於order_inserts的存在,或者可能與此無關)會足夠聰明,可以返回並修改已經生成的SQL插入語句。

  • T2> em.persist(e1); >生成一個endnode_id為null的插入語句。

  • T3> em.persist(n1); > n1與瞬態端點n2有關系。 我該怎么辦? 沒有cacade,所以我無法保存它,因此拋出異常。

我已將問題集中在一個最小的示例上,這確實是一個錯誤

考慮具有兩個屬性的簡單實體節點

  • 名稱( 需要具有@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 )(在persist或flush / commit上)
  • 或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上生成的,第二條是在flush / commit上生成的; 但是,在這種情況下,我希望在驗證實體之后立即生成一個插入語句(然后在刷新/提交時間)。

總之,我看到兩種可能的解決方案:

  • 在persist()中運行驗證器
  • 將語句生成推遲到刷新/提交時間

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM