我有这些实体:
@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 | |
+-----------------------------+--------------+------+-----+---------+----------------+
我也有这些要求:
Content.current
始终是Content.revisionList
的成员(将Content.current
视为“指针”)。ContentRevision
Content
Content
,其中包含初始ContentRevision
(级联持续存在)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),也许是别的,但肯定是不安全。
我想知道是否有办法获得相同的实例。
答案 0 :(得分:4)
当同一个实体被引用两次时级联合并是否安全?
是。如果您管理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
。
您可以改为加载列表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;
根据要求,我写了一个测试。有关我使用的Content
和ContentRevision
的实施,请参阅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)和,只要content
和current
引用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
很难在我的机器上重现这种情况,但我得到了它的工作。这是我到目前为止所做的:
我更改了所有@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);
});
。