[英]JPA/Hibernate cannot understand persist order
我試圖了解jpa / hibernate的“魔術”在實際中是如何工作的,以避免將來(以及常見的)陷阱。
因此,我創建了一些簡單的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
調用)在提交時失敗,並且違反了EDGE.ENDNODE_ID
的數據庫空約束。
我認為這不應該發生,並且我相信:
e1
應該同時與n1
和n2
鏈接。 test3調用persist
較晚,直接在em.persist(n1);
上失敗em.persist(n1);
行(而不是提交)。
我認為這也不應該發生。
引用臨時實體的e1.endNode
會拋出一個異常(級聯),而在test2中,即使e1.endNode
為NULL也不會在persist上調用任何異常。
有人可以解釋為什么在提交時拋出test2異常,而在持久化時拋出test3(使用order_inserts時 )?
在提交之前,Hibernate是否不應該緩存(和順序)插入語句?
UPDATE
我不需要修復 ,我需要一個解釋。 我將嘗試使問題更清楚:
e1.setEndNode(n2)
,但空值到達了數據庫? 調用persist並跟蹤終端節點n2
之后不應該管理e1
嗎? 我會嘗試回答自己:
在持續存在之后,被管理的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,所以我無法保存它,因此拋出異常。
我已將問題集中在一個最小的示例上,這確實是一個錯誤 。
考慮具有兩個屬性的簡單實體節點 :
然后考慮這個測試:
@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上) 假設預期行為是拋出的驗證異常,則在刷新/提交時在實體上執行驗證器,但此時實體已設置了“名稱”。
然后,這導致要驗證的實體與要執行的生成語句之間不同步,從而使驗證返回誤報。
為了說明這一點,請考慮另一個簡單的測試:
@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上生成的; 但是,在這種情況下,我希望在驗證實體之后立即生成一個插入語句(然后在刷新/提交時間)。
總之,我看到兩種可能的解決方案:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.