JPA与同一实体

时间:2015-12-02 09:45:21

标签: java hibernate jpa jpa-2.1

我有这些实体:

@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 mapping:

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
  3. 添加新的Content
  4. 用户可以添加新的Content,其中包含初始ContentRevision(级联持续存在)
  5. 用户可以更改Content.current(移动“指针”)
  6. 用户可以修改Content.current.textData,但可以保存Content(级联合并)
  7. 用户可以删除ContentRevision
  8. 用户可以删除Content(级联删除至ContentRevision
  9. 现在,我的问题是

    1. 这是最好的方法吗?任何最佳做法?
    2. 当同一个实体被引用两次时级联合并是否安全?
      Content.current也是Content.revisionList[i]
    3. Content.currentContent.revisionList[i]是同一个实例吗?
      Content.current == Content.revisionList[i]?)
    4. 由于

      @ 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. 如果我在当前禁用级联,则更改会丢失。

      3. 奇怪的是,在容器外运行此测试会导致成功执行。

        也许它是延迟加载(hibernate.enable_lazy_load_no_trans = true),也许是别的,但肯定是不安全

        我想知道是否有办法获得相同的实例。

1 个答案:

答案 0 :(得分:4)

  

当同一个实体被引用两次时级联合并是否安全?

是。如果您管理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

您可以改为加载列表eagerly,如果这是您想要的。或者您可以定义entity graph以便能够在不同查询中支持这两种策略。

  

用户可以修改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;

更新

根据要求,我写了一个测试。有关我使用的ContentContentRevision的实施,请参阅this gist

我不得不做出一个重要的改变:Content.current不能真正@NotNull,尤其不是DB字段,因为如果是,那么我们就无法持久保存内容和版本时间,因为两者都没有身份证明。因此,该字段最初必须为NULL

作为一种解决方法,我将以下方法添加到Content

@Transient // ignored in JPA
@AssertTrue // javax.validation
public boolean isCurrentRevisionInList() {
    return current != null && getRevisionList().contains(current);
}

此处验证器确保始终存在非空current修订版,它包含在修订列表中。

现在这是我的测试。

这个证明参考文献是相同的(问题3),只要contentcurrent引用revisionList[0]就足够了@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)); } 同一个实例(问题2):

@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

更新2

很难在我的机器上重现这种情况,但我得到了它的工作。这是我到目前为止所做的:

我更改了所有@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

错误消失了:-) ... 因为我删除了Handlebars.registerHelper('toJSON', function(obj) { return JSON.stringify(obj, null, 3); });