天天看點

EntityManager無法remove entity的問題

今天遇到一個奇怪的事情,利用EntityManager.remove(entity)方法删除一個entity時,删不掉,也不報錯。後來經過多方查證,解決了這個問題。

ERD

<a href="http://s3.51cto.com/wyfs02/M00/7F/49/wKioL1cY4PXDaMjhAAA3p6ZllUk448.png" target="_blank"></a>

Entity定義

<code>------------- 第一個Entity A ---------------</code>

<code>@Entity</code>

<code>public</code> <code>class</code> <code>A {</code>

<code>    </code><code>@Id</code>

<code>    </code><code>private</code> <code>Long id;</code>

<code>    </code> 

<code>    </code><code>@Column</code><code>(nullable = </code><code>false</code><code>, unique = </code><code>true</code><code>, length = </code><code>60</code><code>)</code>

<code>    </code><code>private</code> <code>String internalKey;</code>

<code>    </code><code>@OneToMany</code><code>(mappedBy = </code><code>"b"</code><code>, cascade = CascadeType.ALL, orphanRemoval = </code><code>true</code><code>)</code>

<code>    </code><code>private</code> <code>List&lt;B&gt; bs = </code><code>new</code> <code>ArrayList&lt;&gt;();</code>

<code>    </code><code>...</code>

<code>}</code>

<code>------------- 第二個Entity B ---------------</code>

<code>public</code> <code>class</code> <code>B {</code>

<code>    </code><code>@ManyToOne</code>

<code>    </code><code>@JoinColumn</code><code>(name = </code><code>"A_internalKey"</code><code>, referencedColumnName = </code><code>"internalKey"</code><code>)</code>

<code>    </code><code>private</code> <code>A a;</code>

資料

<code>Table A:</code>

<code>id       internalKey</code>

<code>-------- -------------</code>

<code>1        a1</code>

<code>Table B:</code>

<code>id       A_internalKey</code>

<code>2        a1</code>

問題

按照多年SQL腳本操作資料的經驗,直接從B表中删除記錄b(id:2)是可行的。A表上不存在任何對B表的外鍵引用,是以可以直接删除B表上的資料,資料庫管理系統不會不開心。但是,使用JPA中EntityManager的remove(entity)方法來删除b(id:2)時,問題發生了。remove根本删不掉b(id:2)記錄,别說資料庫中的記錄了,連SQL語句都沒有從JEE container中發出來。而且,更要命的是,沒!有!報!錯!

我做了一個替換方案,用JPQL語句直接删除B(id:2),結果成功了。呵呵,到此可不算完結,不然我也不用大費周章的把這件事情記錄下來。在删除 B(id:2)之後,我又嘗試儲存對A所做的變更,這麼一儲存,又出問題了。JPA報錯,說是B(id:2)找不到,我暈。這又是什麼情 況,B(id:2)明明已經被我删掉了,怎麼在persist A的時候JPA卻要去檢查一個已經被删掉的object?我确信在用JPQL删掉了B(id:2)後,我手動從A.bs集合中剔除了B(id:2),為啥 這個B(id:2)陰魂不散呢?

分析

在翻閱了一些文檔後,我隐約意識到,問題應該與entity的幾種狀态(尤其是detached狀态)以及O/R Mapping架構中的緩存有關。說白了,就是程式哪裡産生資料不一緻了。一般,之是以産生這種不一緻問題可能與受管對象的狀态、生命周期或是通路範圍等有關。那麼,代入JPA中考慮,對應的應該是Entity的生命周期或通路機制(緩存機制)。

繼續深究發現,這個issue是由于多個方面綜合作用下産生的。

首先,問題的最關鍵之處:A與B的bidirectional OneToMany(雙向一對多關系)。

這其實很好了解,就像Java中的垃圾回收機制一樣,被用到的Object不會被GC。同理,被引用的child,也就是這個B(id:2)啦,一直被A(id:1)引用着呢,JPA怎麼會讓你把他幹掉?!。前面未曾提及,在删除B(id:2)之前,A(id:1)被JPA讀取過。當我試圖删除B(id:2)時A(id:1)應該還在JPA的緩存裡待着。根據Entity上的annotation标注,A(id:1)應該同時保有B(id:1)、B(id:2)的引用(就是那個List&lt;B&gt; bs集合中的兩個元素)。JPA的remove出于某種保護,并不會讓你把被引用的B(id:2)删掉。

當然,如果你執意要删除,那麼可以用entityManager.createQuery("DELETE FROM B WHERE B.id=2").executeUpdate();來強行删除指定的資料庫記錄。因為createQuery().executeUpdate()會向DBMS發送指定的sql,如果有報錯,異常會由DBMS通過底層JDBC報給JPA架構最終通過EntityManager冒出來。我就是用了這種方法強行把B(id:2)給幹掉了。

接下來要說的,就是因素二:緩存與實際資料庫不一緻

看上面的那段标紅的内容。是不是想到了什麼?在删除B(id:2)之前,A(id:1)帶着對B(id:1)和B(id:2)的引用一直待在緩存裡。當B(id:2)被我用JPQL強行删除之後,并沒有任何代碼去更新緩存裡的A(id:1),是以A(id:1)上應該還有B(id:2)的引用。接下來,要persist A(id:1)的改動。雖然我後來手動做了A.bs.remove(B(id:2))(從bs集合中剔除了B(id:2)的引用),但很遺憾,A(id:1)已經處于detached狀态(即遊離狀态,姑且把已經處于遊離狀态的A(id:1)叫做a(id:1))。對一個已經處于遊離狀态的object進行的改動,不會映射到對應的Entity上,換句話說,不論我怎樣操作a(id:1),在JPA緩存中的A(id:1)不會被更新。而且,戲劇性的一幕發生了,當我嘗試着去persist一個遊離對象a(id:1)時,JPA通過a(id:1).equals(A(id:1))的比較,認為a(id:1) == A(id:1),因為兩個對象的id一樣,hashcode一樣,是以JPA從緩存中找到A(id:1),試圖再次persist一遍,接下來的事情也就不用我說了,JPA報錯,并提示我找不到B(id:2)。(什麼?為什麼會去找B(id:2)?哦,那是因為A上的Cascade定義,)

解決方案

我這裡有兩種解決方案:

1、以更新A為起始點,剔除B(id:2)後,persist A。由于A上設定的Cascade,在更新A的同時,JPA會級聯删除B(id:2)

2、可以強行删除B(id:2),但在對A(id:1)進行任何操作前,先去fecth一下A(id:1),也就是強行重新整理一下JPA的緩存。

我個人推薦第一種方案。

本文轉自 rickqin 51CTO部落格,原文連結:http://blog.51cto.com/rickqin/1766492