Pertanyaan Batalkan Pelanggaran (NOT NULL) saat menggunakan gabungan tetapi tidak berlanjut


Saya telah mengalami beberapa perilaku tak terduga saat menggunakan JPA.

Dalam model data saya, saya punya dua entitas, Foo dan Bar. Foos dan Barsebenarnya 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).


4
2018-04-13 20:57


asal


Jawaban: