使用JPA时,我遇到了一些意想不到的行为。
在我的数据模型中,我有两个实体,Foo
和Bar
。 Foo
和Bar
实际上有多对多关系 - 但Bar
中Foo
的每次出现都包含其他信息,这些信息已导致我们避免使用简单的JoinTable方法。因此,我们使用另一个实体FooBar
来表示Bar
中Foo
的出现。这是一些例子。 (请注意,除了关系成员/方法之外,我已经省略了所有内容。)
@Entity
public class Foo
{
@OneToMany(mappedBy = FooBar.FOO,
orphanRemoval = true,
fetch = FetchType.LAZY,
cascade = { CascadeType.ALL })
private Set<FooBar> fooBars = new HashSet<>();
public void addFooBar(Collection<FooBar> fooBars)
{
Set<FooBar> previous = new HashSet<>(this.fooBars);
this.fooBars.addAll(fooBars);
for ( FooBar fooBar : fooBars )
{
if ( !previous.contains(fooBar) )
{
fooBar.setFoo(this);
}
}
}
public void removeFooBar(FooBar ... fooBars)
{
removeFooBar(Arrays.asList(fooBars));
}
public void removeFooBar(Collection<FooBar> fooBars)
{
Set<FooBar> previous = new HashSet<>(this.fooBars);
this.fooBars.removeAll(fooBars);
for ( FooBar fooBar : fooBars )
{
if ( previous.contains(fooBar) )
{
fooBar.setFoo(null);
}
}
}
public Set<FooBar> getFooBars()
{
return Collections.unmodifiableSet(fooBars);
}
}
@Entity
public class Bar
{
@OneToMany(mappedBy = FooBar.BAR,
orphanRemoval = true,
fetch = FetchType.LAZY,
cascade = { CascadeType.ALL })
private Set<FooBar> fooBars = new HashSet<>();
public void addFooBar(FooBar ... fooBars)
{
addFooBar(Arrays.asList(fooBars));
}
public void addFooBar(Collection<FooBar> fooBars)
{
Set<FooBar> previous = new HashSet<>(this.fooBars);
this.fooBars.addAll(fooBars);
for ( FooBar fooBar : fooBars )
{
if ( !previous.contains(fooBar) )
{
fooBar.setBar(this);
}
}
}
public void removeFooBar(FooBar ... fooBars)
{
removeFooBar(Arrays.asList(fooBars));
}
public void removeFooBar(Collection<FooBar> fooBars)
{
Set<FooBar> previous = new HashSet<>(this.fooBars);
this.fooBars.removeAll(fooBars);
for ( FooBar fooBar : fooBars )
{
if ( previous.contains(fooBar) )
{
fooBar.setBar(null);
}
}
}
public Set<FooBar> getFooBars()
{
return Collections.unmodifiableSet(fooBars);
}
}
@Entity
public class FooBar
{
public static final String FOO = "foo";
public static final String BAR = "bar";
@ManyToOne(fetch = FetchType.LAZY,
cascade = { CascadeType.PERSIST, CascadeType.MERGE },
optional = false)
@JoinColumn(name = FOO,
nullable = false)
private Foo foo;
@ManyToOne(fetch = FetchType.LAZY,
cascade = { CascadeType.PERSIST, CascadeType.MERGE },
optional = false)
@JoinColumn(name = BAR,
nullable = false)
private Bar bar;
public Foo getFoo()
{
return foo;
}
public void setFoo(Foo foo)
{
// If we are no longer associated with a previous report, we must
// remove ourselves from it.
Foo previous = this.foo;
if ( null != previous && !previous.equals(foo) )
{
previous.removeFooBar(this);
}
// Next, we need to set our own value.
this.foo = foo;
// Finally, we need to make sure that we're added to the new report
if ( null != this.foo && !this.foo.equals(previous) )
{
this.foo.addFooBar(this);
}
}
public Bar getBar()
{
return bar;
}
public void setBar(Bar bar)
{
// If we are no longer associated with a previous report, we must
// remove ourselves from it.
Bar previous = this.bar;
if ( null != previous && !previous.equals(bar) )
{
previous.removeFooBar(this);
}
// Next, we need to set our own value.
this.bar = bar;
// Finally, we need to make sure that we're added to the new report
if ( null != this.bar && !this.bar.equals(previous) )
{
this.bar.addFooBar(this);
}
}
}
当我有一个预先存在的Foo
和Bar
时,我的问题就出现了,但是想要一个新的FooBar
引用它们。目前,我们的DAO实现提供了使用dao.createOrUpdate
的方法entityManager.merge
。但是,此调用将因null检查违规而失败。但是,如果我手动访问实体管理器并使用entityManager.persist
,我发现一切正常。这是一些测试代码:
public class Example
{
private static final Logger LOG = LoggerFactory.getLogger(Example.class);
@Test
public void failWithMerge()
{
// Make a Foo and a Bar that already exist.
Long fooId;
Long barId;
try ( ExampleDAO dao = new ExampleDAO() )
{
EntityManager manager = dao.getEntityManager();
manager.getTransaction().begin();
Foo foo = new Foo();
foo.setFooValue("This is a foo value");
fooId = manager.merge(foo).getId();
Bar bar = new Bar();
bar.setBarValue("This is a bar value");
barId = manager.merge(bar).getId();
manager.getTransaction().commit();
}
// Make a new FooBar to associate them
Long id;
try ( ExampleDAO dao = new ExampleDAO() )
{
EntityManager manager = dao.getEntityManager();
Foo foo = dao.getOne(Foo.class, fooId);
Bar bar = dao.getOne(Bar.class, barId);
FooBar fooBar = new FooBar();
fooBar.setFoo(foo);
fooBar.setBar(bar);
LOG.info("============ MERGE ============");
manager.getTransaction().begin();
id = manager.merge(fooBar).getId();
manager.getTransaction().commit();
}
finally
{
LOG.info("===============================");
}
}
@Test
public void worksWithPersist()
{
// Make a Foo and a Bar that already exist.
Long fooId;
Long barId;
try ( ExampleDAO dao = new ExampleDAO() )
{
EntityManager manager = dao.getEntityManager();
manager.getTransaction().begin();
Foo foo = new Foo();
foo.setFooValue("This is a foo value");
fooId = manager.merge(foo).getId();
Bar bar = new Bar();
bar.setBarValue("This is a bar value");
barId = manager.merge(bar).getId();
manager.getTransaction().commit();
}
// Make a new FooBar to associate them
Long id;
try ( ExampleDAO dao = new ExampleDAO() )
{
EntityManager manager = dao.getEntityManager();
Foo foo = dao.getOne(Foo.class, fooId);
Bar bar = dao.getOne(Bar.class, barId);
FooBar fooBar = new FooBar();
fooBar.setFoo(foo);
fooBar.setBar(bar);
LOG.info("============ PERSIST ============");
manager.getTransaction().begin();
manager.persist(fooBar);
id = fooBar.getId();
manager.getTransaction().commit();
}
finally
{
LOG.info("=================================");
}
}
}
通过启用其他日志输出,我发现在使用merge
时,它实际上是在尝试插入FooBar
两次!以下是使用merge
时的相关日志输出:
1609 [main] INFO my.example.Example - ============ MERGE ============
1610 [main] DEBUG org.hibernate.SQL -
insert
into
FooBar
(id, bar, foo, info, version)
values
(default, ?, ?, ?, ?)
1610 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
1610 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [BIGINT] - 1
1610 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [VARCHAR] - <null>
1610 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [4] as [BIGINT] - 0
1612 [main] DEBUG org.hibernate.SQL -
insert
into
FooBar
(id, bar, foo, info, version)
values
(default, ?, ?, ?, ?)
1612 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - <null>
1613 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [BIGINT] - <null>
1613 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [VARCHAR] - <null>
1613 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [4] as [BIGINT] - 0
1614 [main] WARN org.hibernate.engine.jdbc.spi.SqlExceptionHelper - SQL Error: -10, SQLState: 23502
1614 [main] ERROR org.hibernate.engine.jdbc.spi.SqlExceptionHelper - integrity constraint violation: NOT NULL check constraint; SYS_CT_10097 table: FOOBAR column: BAR
1616 [main] INFO my.example.Example - ===============================
与使用persist
:
1636 [main] INFO my.example.Example - ============ PERSIST ============
1636 [main] DEBUG org.hibernate.SQL -
insert
into
FooBar
(id, bar, foo, info, version)
values
(default, ?, ?, ?, ?)
1636 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 2
1636 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [BIGINT] - 2
1637 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [VARCHAR] - <null>
1637 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [4] as [BIGINT] - 0
1642 [main] DEBUG org.hibernate.SQL -
update
Foo
set
fooValue=?,
version=?
where
id=?
and version=?
1642 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - This is a foo value
1643 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [BIGINT] - 1
1643 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - 2
1643 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [4] as [BIGINT] - 0
1645 [main] DEBUG org.hibernate.SQL -
update
Bar
set
barValue=?,
version=?
where
id=?
and version=?
1646 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - This is a bar value
1646 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [BIGINT] - 1
1646 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - 2
1646 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [4] as [BIGINT] - 0
1646 [main] INFO my.example.Example - =================================
由于其他几个原因,我们决定在DAO实施中使用merge
,我更愿意找到一种方法,使用merge
使其正常工作。我希望你们中的一个人可以提供一些见解。
最后,对于它的价值,我们的JPA提供程序是Hibernate - 尽管你可以看到我在本例中仅限于JPA注释。我的测试在HSQLDB中运行,而我们的生产代码使用PostgreSQL。
谢谢,
柯蒂斯
更新
我认为我最终要做的是改变dao.createOrUpdate
(使用merge
)的实现。上面未包含的内容是Foo
,Bar
和FooBar
都提供getId()
方法。因此,我想我会这样做:
public <T extends PersistentObject> T createOrUpdate(T object)
{
if ( null == object.getId() )
{
manager.persist(object);
return object;
}
return manager.merge(object);
}
(PersistentObject
是所有对象的通用接口。)