[英]JPA double relation with the same Entity
我有這些實體:
@Entity
public class Content extends AbstractEntity
{
@NotNull
@OneToOne(optional = false)
@JoinColumn(name = "CURRENT_CONTENT_REVISION_ID")
private ContentRevision current;
@OneToMany(mappedBy = "content", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ContentRevision> revisionList = new ArrayList<>();
}
@Entity
public class ContentRevision extends AbstractEntity
{
@NotNull
@ManyToOne(optional = false)
@JoinColumn(name = "CONTENT_ID")
private Content content;
@Column(name = "TEXT_DATA")
private String textData;
@Temporal(TIMESTAMP)
@Column(name = "REG_DATE")
private Date registrationDate;
}
這是db映射:
CONTENT
+-----------------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------------------+--------------+------+-----+---------+----------------+
| ID | bigint(20) | NO | PRI | NULL | auto_increment |
| CURRENT_CONTENT_REVISION_ID | bigint(20) | NO | MUL | NULL | |
+-----------------------------+--------------+------+-----+---------+----------------+
CONTENT_REVISION
+-----------------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------------------+--------------+------+-----+---------+----------------+
| ID | bigint(20) | NO | PRI | NULL | auto_increment |
| REG_DATE | datetime | YES | | NULL | |
| TEXT_DATA | longtext | YES | | NULL | |
| CONTENT_ID | bigint(20) | NO | MUL | NULL | |
+-----------------------------+--------------+------+-----+---------+----------------+
我也有這些要求:
Content.current
總是成員Content.revisionList
(想想Content.current
為“指針”)。 ContentRevision
添加到現有Content
ContentRevision
添加新Content
(級聯持久) Content.current
(移動“指針”) Content.current.textData
,但保存Content
(級聯合並) ContentRevision
Content
(級聯刪除到ContentRevision
) 現在,我的問題是 :
Content.current
也是Content.revisionList[i]
) Content.current
和Content.revisionList[i]
是同一個實例嗎? Content.current
== Content.revisionList[i]
?) 謝謝
@ jabu.10245我非常感謝你的努力。 謝謝,真的。
但是,測試中存在一個有問題(缺失)的情況:當您使用CMT在容器內運行時:
@RunWith(Arquillian.class)
public class ArquillianTest
{
@PersistenceContext
private EntityManager em;
@Resource
private UserTransaction utx;
@Deployment
public static WebArchive createDeployment()
{
// Create deploy file
WebArchive war = ShrinkWrap.create(WebArchive.class, "test.war");
war.addPackages(...);
war.addAsResource("persistence-arquillian.xml", "META-INF/persistence.xml");
war.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
// Show the deploy structure
System.out.println(war.toString(true));
return war;
}
@Test
public void testDetached()
{
// find a document
Document doc = em.find(Document.class, 1L);
System.out.println("doc: " + doc); // Document@1342067286
// get first content
Content content = doc.getContentList().stream().findFirst().get();
System.out.println("content: " + content); // Content@511063871
// get current revision
ContentRevision currentRevision = content.getCurrentRevision();
System.out.println("currentRevision: " + currentRevision); // ContentRevision@1777954561
// get last revision
ContentRevision lastRevision = content.getRevisionList().stream().reduce((prev, curr) -> curr).get();
System.out.println("lastRevision: " + lastRevision); // ContentRevision@430639650
// test equality
boolean equals = Objects.equals(currentRevision, lastRevision);
System.out.println("1. equals? " + equals); // true
// test identity
boolean same = currentRevision == lastRevision;
System.out.println("1. same? " + same); // false!!!!!!!!!!
// since they are not the same, the rest makes little sense...
// make it dirty
currentRevision.setTextData("CHANGED " + System.currentTimeMillis());
// perform merge in CMT transaction
utx.begin();
doc = em.merge(doc);
utx.commit(); // --> ERROR!!!
// get first content
content = doc.getContentList().stream().findFirst().get();
// get current revision
currentRevision = content.getCurrentRevision();
System.out.println("currentRevision: " + currentRevision);
// get last revision
lastRevision = content.getRevisionList().stream().reduce((prev, curr) -> curr).get();
System.out.println("lastRevision: " + lastRevision);
// test equality
equals = Objects.equals(currentRevision, lastRevision);
System.out.println("2. equals? " + equals);
// test identity
same = currentRevision == lastRevision;
System.out.println("2. same? " + same);
}
}
因為它們不一樣:
如果我在兩個屬性上啟用級聯,則拋出異常
java.lang.IllegalStateException: Multiple representations of the same entity [it.shape.edea2.jpa.ContentRevision#1] are being merged. Detached: [ContentRevision@430639650]; Detached: [ContentRevision@1777954561]
如果我在當前禁用級聯,則更改會丟失。
奇怪的是,在容器外運行此測試會導致成功執行。
也許它是延遲加載(hibernate.enable_lazy_load_no_trans = true),也許是其他東西,但它絕對不是安全的 。
我想知道是否有辦法獲得相同的實例。
當同一實體被引用兩次時級聯合並是否安全?
是。 如果您管理Content
實例,那么它的Content.revisionList
和Content.current
也會被管理。 在刷新實體管理器時,將保留其中任何一個的更改。 您不必手動調用EntityManager.merge(...)
,除非您正在處理需要合並的瞬態對象。
如果您創建新的ContentRevision
,則調用persist(...)
而不是與該新實例merge(...)
,並確保它具有對父Content
的托管引用,並將其添加到內容列表中。
Content.current和Content.revisionList [i]是同一個實例嗎?
是的,應該是。 測試它是肯定的。
Content.current始終是Content.revisionList的成員(將Content.current視為“指針”)。
您可以使用檢查約束在SQL中檢入; 或者在Java中,盡管您必須確保獲取了revisionList
。 默認情況下,它是延遲獲取的,這意味着如果訪問getRevisionList()
方法,Hibernate將為此列表運行另一個查詢。 為此,您需要一個正在運行的事務,否則您將獲得一個LazyInitializationException
。
如果那是你想要的,你可以急切地加載列表。 或者,您可以定義實體圖 ,以便能夠在不同查詢中支持這兩種策略。
用戶可以修改Content.current.textData,但保存內容(級聯合並)
請參閱上面的第一段,Hibernate應自動保存對任何托管實體的更改。
用戶可以刪除ContentRevision
if (content.getRevisionList().remove(revision))
entityManager.remove(revision);
if (revision.equals(content.getCurrentRevision())
content.setCurrentRevision(/* to something else */);
用戶可以刪除內容(級聯刪除到ContentRevision)
在這里,我寧願確保在數據庫模式中,例如
FOREIGN KEY (content_id) REFERENCES content (id) ON DELETE CASCADE;
根據要求,我寫了一個測試。 有關我使用的Content
和ContentRevision
的實現,請參閱此要點 。
我不得不做一個重要的改變: Content.current
不能真正成為@NotNull
,尤其不是DB字段,因為如果是,那么我們不能同時保留內容和修訂,因為它們都沒有ID 。 因此,最初必須允許該字段為NULL
。
作為一種解決方法,我在Content
添加了以下方法:
@Transient // ignored in JPA
@AssertTrue // javax.validation
public boolean isCurrentRevisionInList() {
return current != null && getRevisionList().contains(current);
}
這里驗證器確保始終存在非空的current
修訂版,並且它包含在修訂列表中。
現在這是我的測試。
這個證明了引用是相同的(問題3) 並且足以在current
和revisionList[0]
引用相同實例(問題2)的情況下持久化content
:
@Test @InSequence(0)
public void shouldCreateContentAndRevision() throws Exception {
// create java objects, unmanaged:
Content content = Content.create("My first test");
assertNotNull("content should have current revision", content.getCurrent());
assertSame("content should be same as revision's parent", content, content.getCurrent().getContent());
assertEquals("content should have 1 revision", 1, content.getRevisionList().size());
assertSame("the list should contain same reference", content.getCurrent(), content.getRevisionList().get(0));
// persist the content, along with the revision:
transaction.begin();
entityManager.joinTransaction();
entityManager.persist(content);
transaction.commit();
// verify:
assertEquals("content should have ID 1", Long.valueOf(1), content.getId());
assertEquals("content should have one revision", 1, content.getRevisionList().size());
assertNotNull("content should have current revision", content.getCurrent());
assertEquals("revision should have ID 1", Long.valueOf(1), content.getCurrent().getId());
assertSame("current revision should be same reference", content.getCurrent(), content.getRevisionList().get(0));
}
下一步確保在加載實體后它仍然是真的:
@Test @InSequence(1)
public void shouldLoadContentAndRevision() throws Exception {
Content content = entityManager.find(Content.class, Long.valueOf(1));
assertNotNull("should have found content #1", content);
// same checks as before:
assertNotNull("content should have current revision", content.getCurrent());
assertSame("content should be same as revision's parent", content, content.getCurrent().getContent());
assertEquals("content should have 1 revision", 1, content.getRevisionList().size());
assertSame("the list should contain same reference", content.getCurrent(), content.getRevisionList().get(0));
}
即使更新它:
@Test @InSequence(2)
public void shouldAddAnotherRevision() throws Exception {
transaction.begin();
entityManager.joinTransaction();
Content content = entityManager.find(Content.class, Long.valueOf(1));
ContentRevision revision = content.addRevision("My second revision");
entityManager.persist(revision);
content.setCurrent(revision);
transaction.commit();
// re-load and validate:
content = entityManager.find(Content.class, Long.valueOf(1));
// same checks as before:
assertNotNull("content should have current revision", content.getCurrent());
assertSame("content should be same as revision's parent", content, content.getCurrent().getContent());
assertEquals("content should have 2 revisions", 2, content.getRevisionList().size());
assertSame("the list should contain same reference", content.getCurrent(), content.getRevisionList().get(1));
}
SELECT * FROM content;
id | version | current_content_revision_id
----+---------+-----------------------------
1 | 2 | 2
很難在我的機器上重現這種情況,但我得到了它的工作。 這是我到目前為止所做的:
我更改了所有@OneToMany
關系以使用延遲提取(默認)並重新運行以下測試用例:
@Test @InSequence(3)
public void shouldChangeCurrentRevision() throws Exception {
transaction.begin();
entityManager.joinTransaction();
Document document = entityManager.find(Document.class, Long.valueOf(1));
assertNotNull(document);
assertEquals(1, document.getContentList().size());
Content content = document.getContentList().get(0);
assertNotNull(content);
ContentRevision revision = content.getCurrent();
assertNotNull(revision);
assertEquals(2, content.getRevisionList().size());
assertSame(revision, content.getRevisionList().get(1));
revision.setTextData("CHANGED");
document = entityManager.merge(document);
content = document.getContentList().get(0);
revision = content.getCurrent();
assertSame(revision, content.getRevisionList().get(1));
assertEquals("CHANGED", revision.getTextData());
transaction.commit();
}
測試通過了懶惰的提取。 請注意,延遲提取要求在事務中執行它。
出於某種原因,你正在編輯的內容修改的實例是不一樣的人在一個一對多的名單。 為了重現我已修改我的測試,如下所示:
@Test @InSequence(4)
public void shouldChangeCurrentRevision2() throws Exception {
transaction.begin();
Document document = entityManager.find(Document.class, Long.valueOf(1));
assertNotNull(document);
assertEquals(1, document.getContentList().size());
Content content = document.getContentList().get(0);
assertNotNull(content);
ContentRevision revision = content.getCurrent();
assertNotNull(revision);
assertEquals(2, content.getRevisionList().size());
assertSame(revision, content.getRevisionList().get(1));
transaction.commit();
// load another instance, different from the one in the list:
revision = entityManager.find(ContentRevision.class, revision.getId());
revision.setTextData("CHANGED2");
// start another TX, replace the "current revision" but not the one
// in the list:
transaction.begin();
document.getContentList().get(0).setCurrent(revision);
document = entityManager.merge(document); // here's your error!!!
transaction.commit();
content = document.getContentList().get(0);
revision = content.getCurrent();
assertSame(revision, content.getRevisionList().get(1));
assertEquals("CHANGED2", revision.getTextData());
}
在那里,我得到了你的錯誤。 然后我修改了@OneToMany
映射的級聯設置:
@OneToMany(mappedBy = "content", cascade = { PERSIST, REFRESH, REMOVE }, orphanRemoval = true)
private List<ContentRevision> revisionList;
錯誤消失了:-) ... 因為我刪除了CascadeType.MERGE
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.