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