Saya telah mengalami beberapa perilaku tak terduga saat menggunakan JPA.
Dalam model data saya, saya punya dua entitas, Foo
dan Bar
. Foo
s dan Bar
sebenarnya memiliki hubungan banyak-ke-banyak - tetapi setiap terjadinya a Bar
di sebuah Foo
juga mengandung informasi tambahan yang telah menyebabkan kita menjauhkan pendekatan JoinTable sederhana. Oleh karena itu, kami menggunakan entitas lain, FooBar
, untuk mewakili terjadinya suatu Bar
dalam Foo
. Berikut contohnya. (Harap dicatat saya telah menghilangkan semuanya kecuali anggota / metode hubungan).
@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);
}
}
}
Masalah saya muncul ketika saya sudah ada sebelumnya Foo
dan Bar
, tetapi ingin membuat yang baru FooBar
mereferensikan mereka. Saat ini, penerapan DAO kami menyediakan metode dao.createOrUpdate
yang menggunakan entityManager.merge
. Namun, panggilan ini akan gagal dengan pelanggaran pemeriksaan nihil. Namun, jika saya secara manual mengakses pengelola entitas dan menggunakannya entityManager.persist
, Saya telah menemukan bahwa semuanya bekerja dengan baik. Berikut ini beberapa kode pengujian:
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("=================================");
}
}
}
Dengan mengaktifkan keluaran log tambahan, saya telah menemukan itu, saat menggunakan merge
, sebenarnya mencoba memasukkan FooBar
dua kali! Di sini adalah output log yang bersangkutan saat menggunakan 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 - ===============================
Bandingkan dengan hasil ini yang dihasilkan dari penggunaan 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 - =================================
Kami telah membuat keputusan untuk menggunakan merge
dalam penerapan DAO kami karena beberapa alasan lain, dan saya lebih suka mencari cara untuk membuat ini berfungsi dengan benar merge
. Saya berharap salah satu dari Anda mungkin memberikan beberapa wawasan.
Akhirnya, untuk apa nilainya, penyedia JPA kami adalah Hibernate - meskipun seperti yang Anda lihat, saya telah membatasi diri pada anotasi JPA dalam contoh ini. Pengujian saya berjalan di HSQLDB, sementara kode produksi kami menggunakan PostgreSQL.
Terima kasih,
Curtis
Memperbarui
Saya pikir apa yang akan saya lakukan adalah mengubah implementasi dao.createOrUpdate
(yang digunakan apa merge
). Apa yang tidak termasuk di atas adalah itu Foo
, Bar
, dan FooBar
semua menyediakan getId()
metode. Oleh karena itu, saya pikir saya akan melakukan ini:
public <T extends PersistentObject> T createOrUpdate(T object)
{
if ( null == object.getId() )
{
manager.persist(object);
return object;
}
return manager.merge(object);
}
(PersistentObject
adalah antarmuka umum untuk semua objek).