PersistentObjectException: entitas terpisah dilewatkan untuk bertahan dilemparkan oleh JPA dan Hibernate

237

Saya memiliki model objek JPA-persisten yang berisi hubungan banyak-ke-satu: Accountmemiliki banyak Transactions. A Transactionmemiliki satu Account.

Berikut cuplikan kode:

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    private Account fromAccount;
....

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @OneToMany(cascade = {CascadeType.ALL},fetch= FetchType.EAGER, mappedBy = "fromAccount")
    private Set<Transaction> transactions;

Saya dapat membuat Accountobjek, menambahkan transaksi, dan bertahan Accountobjek dengan benar. Tetapi, ketika saya membuat transaksi, menggunakan Akun yang sudah ada yang sudah ada , dan bertahan dalam Transaksi , saya mendapatkan pengecualian:

Disebabkan oleh: org.hibernate.PersistentObjectException: entitas terpisah dilewatkan untuk bertahan: com.paulsanwald.Akun di org.hibernate.event.internal.DefaultPersistEventListener.onPersist (DefaultPersistEventListener.java:141)

Jadi, saya bisa bertahan Accountyang mengandung transaksi, tetapi bukan Transaksi yang memiliki Account. Saya pikir ini karena Accountmungkin tidak dilampirkan, tetapi kode ini masih memberi saya pengecualian yang sama:

if (account.getId()!=null) {
    account = entityManager.merge(account);
}
Transaction transaction = new Transaction(account,"other stuff");
 // the below fails with a "detached entity" message. why?
entityManager.persist(transaction);

Bagaimana saya bisa menyimpan dengan benar Transaction, yang terkait dengan objek yang sudah ada Account?

Paul Sanwald
sumber
15
Dalam kasus saya, saya sedang mengatur id suatu entitas yang saya coba bertahan menggunakan Entity Manager. Ketika, saya menghapus setter untuk id, itu mulai berfungsi dengan baik.
Rushi Shah
Dalam kasus saya, saya tidak mengatur id, tetapi ada dua pengguna yang menggunakan akun yang sama, salah satunya bertahan entitas (dengan benar), dan kesalahan terjadi ketika yang kedua terakhir mencoba bertahan entitas yang sama, yang sudah bertahan.
sergioFC

Jawaban:

129

Ini adalah masalah konsistensi dua arah yang khas. Ini dibahas dengan baik dalam tautan ini dan juga tautan ini.

Sesuai dengan artikel di 2 tautan sebelumnya, Anda perlu memperbaiki setter Anda di kedua sisi hubungan dua arah. Contoh penyetel untuk sisi Satu ada di tautan ini.

Contoh setter untuk sisi Banyak ada di tautan ini.

Setelah Anda memperbaiki setter Anda, Anda ingin mendeklarasikan tipe akses Entity menjadi "Properti". Praktik terbaik untuk menyatakan jenis akses "Properti" adalah memindahkan SEMUA anotasi dari properti anggota ke getter yang sesuai. Kata hati-hati adalah untuk tidak mencampur jenis akses "Field" dan "Property" dalam kelas entitas jika perilaku tidak ditentukan oleh spesifikasi JSR-317.

Simbol-Simbol
sumber
2
ps: anotasi @Id adalah yang digunakan hibernate untuk mengidentifikasi tipe akses.
Diego Plentz
2
Pengecualiannya adalah: detached entity passed to persistMengapa meningkatkan konsistensi membuatnya berfungsi? Ok, konsistensi telah diperbaiki tetapi objek masih terpisah.
Gilgamesz
1
@ Sam, terima kasih banyak atas penjelasan Anda. Tapi, saya masih belum mengerti. Saya melihat bahwa tanpa setter 'khusus' hubungan dua arah tidak terpuaskan. Tapi, saya tidak mengerti mengapa benda itu terlepas.
Gilgamesz
28
Tolong jangan posting jawaban yang hanya menjawab tautan.
El Mac
6
Tidak dapat melihat bagaimana jawaban ini terkait dengan pertanyaan sama sekali?
Eugen Labun
271

Solusinya sederhana, cukup gunakan CascadeType.MERGEbukan CascadeType.PERSISTatau CascadeType.ALL.

Saya memiliki masalah yang sama dan CascadeType.MERGEtelah bekerja untuk saya.

Saya harap Anda diurutkan.

ochiWlad
sumber
5
Mengejutkan bahwa yang satu bekerja untuk saya juga. Tidak masuk akal karena CascadeType.ALL mencakup semua jenis kaskade lainnya ... WTF? Saya memiliki pegas 4.0.4, pegas data jpa 1.8.0 dan hibernate 4.X .. Apakah ada yang punya pikiran mengapa SEMUA tidak bekerja, tetapi MERGE tidak?
Vadim Kirilchuk
22
@VadimKirilchuk Ini bekerja untuk saya juga dan itu masuk akal. Karena Transaksi TERTENTU, ia juga mencoba ke Akun PERSIST dan itu tidak berfungsi karena Akun sudah ada di db. Tetapi dengan CascadeType.MERGE, Akun tersebut secara otomatis digabungkan.
Gunslinger
4
Ini bisa terjadi jika Anda tidak menggunakan transaksi.
lanoxx
1
solusi lain: cobalah untuk tidak memasukkan objek yang sudah ada :)
Andrii Plotnikov
3
Terima kasih kawan Tidak mungkin untuk menghindari memasukkan objek tetap, jika Anda memiliki batasan untuk kunci referensi menjadi TIDAK NULL. Jadi ini satu-satunya solusi. Terima kasih lagi.
makkasi
22

Hapus cascading dari entitas anakTransaction , seharusnya hanya:

@Entity class Transaction {
    @ManyToOne // no cascading here!
    private Account account;
}

( FetchType.EAGERdapat dihapus dan itu adalah default untuk @ManyToOne)

Itu saja!

Mengapa? Dengan mengatakan "cascade ALL" pada entitas anak, TransactionAnda mengharuskan setiap operasi DB disebarkan ke entitas induk Account. Jika Anda melakukannya persist(transaction), persist(account)akan dipanggil juga.

Tetapi hanya entitas transien (baru) yang dapat diteruskan ke persist( Transactiondalam hal ini). Yang terlepas (atau keadaan tidak transien lainnya) mungkin tidak ( Accountdalam hal ini, karena sudah dalam DB).

Oleh karena itu Anda mendapatkan pengecualian "entitas terpisah yang dilewati untuk bertahan" . The Accountentitas yang dimaksud! Bukan Transactionkau yang menelepon persist.


Anda biasanya tidak ingin menyebarkan dari anak ke orang tua. Sayangnya ada banyak contoh kode dalam buku (bahkan yang bagus) dan melalui internet, yang melakukan hal itu. Saya tidak tahu, mengapa ... Mungkin kadang-kadang hanya disalin berulang tanpa banyak berpikir ...

Coba tebak apa yang terjadi jika Anda menyebut remove(transaction)masih memiliki "cascade ALL" di @ManyToOne itu? The account(btw, dengan semua transaksi lain!) Akan dihapus dari DB juga. Tapi itu bukan niatmu, kan?

Eugen Labun
sumber
Hanya ingin menambahkan, jika niat Anda benar-benar untuk menyelamatkan anak bersama orang tua dan juga menghapus orang tua bersama anak, seperti orang (orang tua) dan alamat (anak) bersama dengan addressId diautogasi secara otomatis oleh DB lalu sebelum memanggil save on Person, buat saja panggilan untuk menyimpan alamat dalam metode transaksi Anda. Dengan cara ini akan disimpan bersama dengan id yang juga dihasilkan oleh DB. Tidak berdampak pada kinerja karena Hibenate masih membuat 2 kueri, kami hanya mengubah urutan kueri.
Vikky
Jika kita tidak dapat melewatkan apa pun maka nilai default apa yang diperlukan untuk semua kasus.
Dhwanil Patel
15

Menggunakan penggabungan berisiko dan rumit, jadi ini solusi yang kotor dalam kasus Anda. Anda harus ingat setidaknya bahwa ketika Anda melewatkan objek entitas untuk digabung, itu berhenti melekat pada transaksi dan sebagai gantinya, entitas yang sekarang dilampirkan dikembalikan. Ini berarti bahwa jika ada yang memiliki objek entitas lama masih dalam kepemilikan mereka, perubahan itu diabaikan secara diam-diam dan dibuang pada komit.

Anda tidak menunjukkan kode lengkap di sini, jadi saya tidak dapat memeriksa ulang pola transaksi Anda. Salah satu cara untuk sampai ke situasi seperti ini adalah jika Anda tidak memiliki transaksi aktif ketika menjalankan penggabungan dan bertahan. Dalam hal ini penyedia persistensi diharapkan untuk membuka transaksi baru untuk setiap operasi JPA yang Anda lakukan dan segera melakukan dan menutupnya sebelum panggilan kembali. Jika demikian, gabungan akan dijalankan dalam transaksi pertama dan kemudian setelah metode gabungan kembali, transaksi diselesaikan dan ditutup dan entitas yang dikembalikan sekarang terlepas. Bertahan di bawahnya kemudian akan membuka transaksi kedua, dan mencoba merujuk ke entitas yang terlepas, memberikan pengecualian. Selalu bungkus kode Anda di dalam transaksi kecuali Anda tahu betul apa yang Anda lakukan.

Menggunakan transaksi yang dikelola kontainer, akan terlihat seperti ini. Perhatikan: ini mengasumsikan metode ini ada di dalam sesi kacang dan dipanggil melalui antarmuka Lokal atau Remote.

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void storeAccount(Account account) {
    ...

    if (account.getId()!=null) {
        account = entityManager.merge(account);
    }

    Transaction transaction = new Transaction(account,"other stuff");

    entityManager.persist(account);
}
Zds
sumber
Saya menghadapi masalah yang sama., Saya telah menggunakan @Transaction(readonly=false)di lapisan layanan., Saya masih mendapatkan masalah yang sama,
Senthil Arumugam SP
Saya tidak dapat mengatakan bahwa saya sepenuhnya memahami mengapa semuanya berjalan seperti ini tetapi menempatkan metode bertahan dan melihat ke pemetaan Entity bersama dalam anotasi Transaksional memperbaiki masalah saya jadi terima kasih.
Deadron
Ini sebenarnya adalah solusi yang lebih baik daripada yang paling terunggul.
Aleksei Maide
13

Mungkin dalam kasus ini Anda mendapatkan accountobjek Anda menggunakan logika gabungan, dan persistdigunakan untuk bertahan objek baru dan itu akan mengeluh jika hierarki memiliki objek yang sudah ada. Anda harus menggunakan saveOrUpdatedalam kasus seperti itu, bukan persist.

dan
sumber
3
itu JPA, jadi saya pikir metode analognya adalah .merge (), tapi itu memberi saya pengecualian yang sama. Agar jelas, Transaksi adalah objek baru, Akun tidak.
Paul Sanwald
@PaulSanwald Menggunakan mergepada transactionobjek yang Anda mendapatkan kesalahan yang sama?
dan
sebenarnya, tidak, saya salah bicara. jika saya .merge (transaksi), maka transaksi tidak bertahan sama sekali.
Paul Sanwald
@ PaulSanwald Hmm, apakah Anda yakin transactionitu tidak bertahan? Bagaimana Anda memeriksanya. Perhatikan bahwa mergemengembalikan referensi ke objek tetap.
dan
objek yang dikembalikan oleh .merge () memiliki id nol. juga, saya melakukan .findAll () setelahnya, dan objek saya tidak ada.
Paul Sanwald
12

Jangan meneruskan id (pk) untuk bertahan metode atau mencoba metode save () bukannya bertahan ().

Angad Bansode
sumber
2
Saran yang bagus! Tetapi hanya jika id dihasilkan. Jika ditugaskan, itu normal untuk mengatur id.
Aldian
ini berhasil untuk saya. Juga, Anda dapat menggunakan TestEntityManager.persistAndFlush()untuk membuat instance dikelola dan persisten kemudian menyinkronkan konteks persistensi ke database yang mendasarinya. Mengembalikan entitas sumber asli
Anyul Rivas
7

Karena ini adalah pertanyaan yang sangat umum, saya menulis artikel ini , yang menjadi dasar jawaban ini.

Untuk memperbaiki masalah, Anda harus mengikuti langkah-langkah ini:

1. Menghapus cascading asosiasi anak

Jadi, Anda harus menghapus @CascadeType.ALLdari @ManyToOneasosiasi. Entitas anak tidak boleh mengalir ke asosiasi orang tua. Hanya entitas induk yang harus mengalir ke entitas anak.

@ManyToOne(fetch= FetchType.LAZY)

Perhatikan bahwa saya mengatur fetchatributnya FetchType.LAZYkarena ingin mengambil sangat buruk untuk kinerja .

2. Atur kedua sisi asosiasi

Setiap kali Anda memiliki asosiasi dua arah, Anda perlu menyinkronkan kedua sisi menggunakan addChilddan removeChildmetode dalam entitas induk:

public void addTransaction(Transaction transaction) {
    transcations.add(transaction);
    transaction.setAccount(this);
}

public void removeTransaction(Transaction transaction) {
    transcations.remove(transaction);
    transaction.setAccount(null);
}

Untuk detail lebih lanjut tentang mengapa penting untuk menyinkronkan kedua ujung dari asosiasi dua arah, periksa artikel ini .

Vlad Mihalcea
sumber
4

Dalam definisi entitas Anda, Anda tidak menentukan @JoinColumn untuk Accountbergabung dengan a Transaction. Anda akan menginginkan sesuatu seperti ini:

@Entity
public class Transaction {
    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    @JoinColumn(name = "accountId", referencedColumnName = "id")
    private Account fromAccount;
}

EDIT: Ya, saya kira itu akan berguna jika Anda menggunakan @Tableanotasi di kelas Anda. Heh. :)

NemesisX00
sumber
2
ya saya tidak berpikir ini dia, sama saja, saya menambahkan @JoinColumn (nama = "fromAccount_id", referencedColumnName = "id") dan tidak berhasil :).
Paul Sanwald
Ya, saya biasanya tidak menggunakan file pemetaan xml untuk memetakan entitas ke tabel, jadi saya biasanya menganggap itu berdasarkan penjelasan. Tetapi jika saya harus menebak, Anda menggunakan hibernate.xml untuk memetakan entitas ke tabel, kan?
NemesisX00
tidak, saya menggunakan data musim semi JPA, jadi itu semua berdasarkan penjelasan. Saya memiliki anotasi "mappedBy" di sisi lain: @OneToMany (cascade = {CascadeType.ALL}, fetch = FetchType.EAGER, mappedBy = "fromAccount")
Paul Sanwald
4

Bahkan jika anotasi Anda dinyatakan dengan benar untuk mengelola hubungan satu-ke-banyak dengan benar, Anda masih dapat menemukan pengecualian yang tepat ini. Saat menambahkan objek anak baru Transaction,, ke model data terlampir Anda harus mengelola nilai kunci utama - kecuali Anda tidak seharusnya melakukannya . Jika Anda memberikan nilai kunci utama untuk entitas anak yang dinyatakan sebagai berikut sebelum memanggil persist(T), Anda akan menemukan pengecualian ini.

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
....

Dalam hal ini, anotasi menyatakan bahwa basis data akan mengelola pembuatan nilai kunci primer entitas saat penyisipan. Memberikannya sendiri (seperti melalui setter Id) menyebabkan pengecualian ini.

Atau, tetapi secara efektif sama, deklarasi anotasi ini menghasilkan pengecualian yang sama:

@Entity
public class Transaction {
    @Id
    @org.hibernate.annotations.GenericGenerator(name="system-uuid", strategy="uuid")
    @GeneratedValue(generator="system-uuid")
    private Long id;
....

Jadi, jangan tetapkan idnilai dalam kode aplikasi Anda ketika sudah dikelola.

dan
sumber
4

Jika tidak ada yang membantu dan Anda masih mendapatkan pengecualian ini, tinjau equals() metode - dan jangan masukkan koleksi anak di dalamnya. Terutama jika Anda memiliki struktur koleksi tertanam yang dalam (mis. A mengandung B, B berisi C, dll.).

Dalam contoh Account -> Transactions:

  public class Account {

    private Long id;
    private String accountName;
    private Set<Transaction> transactions;

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (!(obj instanceof Account))
        return false;
      Account other = (Account) obj;
      return Objects.equals(this.id, other.id)
          && Objects.equals(this.accountName, other.accountName)
          && Objects.equals(this.transactions, other.transactions); // <--- REMOVE THIS!
    }
  }

Dalam contoh di atas hapus transaksi dari equals()cek. Ini karena hibernate akan menyiratkan bahwa Anda tidak mencoba memperbarui objek lama, tetapi Anda meneruskan objek baru untuk bertahan, setiap kali Anda mengubah elemen pada koleksi anak.
Tentu saja solusi ini tidak cocok untuk semua aplikasi dan Anda harus hati-hati merancang apa yang ingin Anda sertakan dalam equalsdan hashCodemetode.

FazoM
sumber
1

Mungkin itu adalah bug OpenJPA, Ketika kembalikan itu mengatur ulang bidang @Version, tetapi pcVersionInit tetap benar. Saya memiliki AbstraceEntity yang menyatakan bidang @Version. Saya bisa mengatasinya dengan mereset bidang pcVersionInit. Tapi itu bukan ide yang bagus. Saya pikir itu tidak berfungsi ketika memiliki kaskade bertahan.

    private static Field PC_VERSION_INIT = null;
    static {
        try {
            PC_VERSION_INIT = AbstractEntity.class.getDeclaredField("pcVersionInit");
            PC_VERSION_INIT.setAccessible(true);
        } catch (NoSuchFieldException | SecurityException e) {
        }
    }

    public T call(final EntityManager em) {
                if (PC_VERSION_INIT != null && isDetached(entity)) {
                    try {
                        PC_VERSION_INIT.set(entity, false);
                    } catch (IllegalArgumentException | IllegalAccessException e) {
                    }
                }
                em.persist(entity);
                return entity;
            }

            /**
             * @param entity
             * @param detached
             * @return
             */
            private boolean isDetached(final Object entity) {
                if (entity instanceof PersistenceCapable) {
                    PersistenceCapable pc = (PersistenceCapable) entity;
                    if (pc.pcIsDetached() == Boolean.TRUE) {
                        return true;
                    }
                }
                return false;
            }
Yaocl
sumber
1

Anda perlu mengatur Transaksi untuk setiap Akun.

foreach(Account account : accounts){
    account.setTransaction(transactionObj);
}

Atau cukup colud (jika sesuai) untuk mengatur id ke nol di banyak sisi.

// list of existing accounts
List<Account> accounts = new ArrayList<>(transactionObj.getAccounts());

foreach(Account account : accounts){
    account.setId(null);
}

transactionObj.setAccounts(accounts);

// just persist transactionObj using EntityManager merge() method.
stakahop
sumber
1
cascadeType.MERGE,fetch= FetchType.LAZY
James Siva
sumber
1
Hai James dan selamat datang, Anda harus mencoba dan menghindari jawaban hanya kode. Harap tunjukkan bagaimana ini menyelesaikan masalah yang dinyatakan dalam pertanyaan (dan kapan itu berlaku atau tidak, level API, dll.).
Maarten Bodewes
Peninjau VLQ: lihat meta.stackoverflow.com/questions/260411/… . jawaban hanya kode tidak pantas dihapus, tindakan yang sesuai adalah memilih "Terlihat OK".
Nathan Hughes
0

Dalam kasus saya, saya melakukan transaksi ketika metode bertahan digunakan. Pada perubahan bertahan untuk menyimpan metode, itu terselesaikan.

H. Ostwal
sumber
0

Jika solusi di atas tidak berfungsi hanya satu kali komentar metode pengambil dan penyetel kelas entitas dan tidak menetapkan nilai id. (Kunci utama) Maka ini akan berhasil.

Shubham
sumber
0

@OneToMany (mappedBy = "xxxx", cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE}) bekerja untuk saya.

SateeshKasaboina
sumber
0

Diselesaikan dengan menyimpan objek dependen sebelum yang berikutnya.

Ini terjadi pada saya karena saya tidak mengatur Id (yang tidak dihasilkan secara otomatis). dan mencoba menyimpan dengan relasi @ManytoOne

Shahid Hussain Abbasi
sumber