簡體   English   中英

JPA與同一實體的雙重關系

[英]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    |                |
+-----------------------------+--------------+------+-----+---------+----------------+

我也有這些要求:

  1. Content.current總是成員Content.revisionList (想想Content.current為“指針”)。
  2. 用戶可以將新ContentRevision添加到現有Content
  3. 用戶可以使用初始ContentRevision添加新Content (級聯持久)
  4. 用戶可以更改Content.current (移動“指針”)
  5. 用戶可以修改Content.current.textData ,但保存Content (級聯合並)
  6. 用戶可以刪除ContentRevision
  7. 用戶可以刪除Content (級聯刪除到ContentRevision

現在,我的問題是

  1. 這是最好的方法嗎? 任何最佳做法?
  2. 當同一實體被引用兩次時級聯合並是否安全?
    Content.current也是Content.revisionList[i]
  3. Content.currentContent.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);
    }
}

因為它們不一樣:

  1. 如果我在兩個屬性上啟用級聯,則拋出異常

     java.lang.IllegalStateException: Multiple representations of the same entity [it.shape.edea2.jpa.ContentRevision#1] are being merged. Detached: [ContentRevision@430639650]; Detached: [ContentRevision@1777954561] 
  2. 如果我在當前禁用級聯,則更改會丟失。

奇怪的是,在容器外運行此測試會導致成功執行。

也許它是延遲加載(hibernate.enable_lazy_load_no_trans = true),也許是其他東西,但它絕對不是安全的

我想知道是否有辦法獲得相同的實例。

當同一實體被引用兩次時級聯合並是否安全?

是。 如果您管理Content實例,那么它的Content.revisionListContent.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;

UPDATE

根據要求,我寫了一個測試。 有關我使用的ContentContentRevision的實現,請參閱此要點

我不得不做一個重要的改變: 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) 並且足以在currentrevisionList[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

更新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.

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