PersistentObjectException: JPA ve Hazırda Beklet tarafından atılan kalıcılığa geçirilen bağımsız varlık


237

Bire bir ilişki içeren bir JPA-kalıcı nesne modeli var: bir Accountçok var Transactions. A'nın Transactionbir tane var Account.

İşte kod pasajı:

@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;

Bir Accountnesne oluşturabiliyorum , ona işlemler ekleyebiliyorum ve Accountnesneyi doğru şekilde koruyabiliyorum . Ben, bir hareket oluşturmak Fakat zaten varolan Hesabı ısrar kullanarak , ve devam eden İşlem , bir istisna olsun:

Nedeni: org.hibernate.PersistentObjectException: bağımsız varlık devam etmek için geçti: org.hibernate.event.internal.DefaultPersistEventListener.onPersist adresindeki com.paulsanwald.Account (DefaultPersistEventListener.java:141)

Yani, Accountişlemleri içeren bir devam edebiliyoruz , ancak bir Account. Bunun Accountekli olmayabilir çünkü olduğunu düşündüm , ama bu kod hala bana aynı istisnayı verir:

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);

TransactionZaten var olan bir Accountnesneyle ilişkili bir a'yı nasıl doğru şekilde kaydedebilirim ?


15
Benim durumumda, Varlık Yöneticisi'ni kullanmaya devam etmeye çalıştığım bir varlığın kimliğini ayarlıyordum. Kimlik için ayarlayıcıyı kaldırdığımda, iyi çalışmaya başladı.
Rushi Şah

Benim durumumda, kimliği ayarlamıyordum, ancak aynı hesabı kullanan iki kullanıcı vardı, bunlardan biri bir varlığı (doğru) ısrar etti ve ikincisi aynı varlığı sürdürmeye çalıştığında hata oluştu, bu zaten devam etti.
sergioFC

Yanıtlar:


129

Bu tipik bir çift yönlü tutarlılık problemidir. İyi tartışılmıştır Bu bağlantıyı yanı sıra bu bağlantı.

Önceki 2 bağlantıdaki makalelere göre, çift yönlü ilişkinin her iki tarafındaki ayarlayıcılarınızı düzeltmeniz gerekir. Bir taraf için örnek ayarlayıcı bu bağlantıda.

Birçok taraf için örnek ayarlayıcı bu bağlantıda.

Ayarlayıcılarınızı düzelttikten sonra Varlık erişim türünü "Mülk" olarak bildirmek istersiniz. "Mülk" erişim türünü bildirmenin en iyi yolu, TÜM ek açıklamaları üye özelliklerinden ilgili alıcılara taşımaktır. Dikkat edilmesi gereken önemli bir nokta, "Field" ve "Property" erişim türlerini varlık sınıfı içinde karıştırmamaktır, aksi takdirde davranış JSR-317 belirtimleriyle tanımlanmamıştır.


2
ps: @Id ek açıklaması, hazırda bekletme modunun erişim türünü tanımlamak için kullandığı açıklamadır.
Diego Plentz

2
İstisna şudur: detached entity passed to persistTutarlılığı artırmak neden işe yaramasını sağlar? Tamam, tutarlılık onarıldı, ancak nesne hala ayrıldı.
Gilgamesz

1
@Sam, açıklaman için çok teşekkürler. Ama hala anlamıyorum. 'Özel' ayarlayıcı olmadan iki yönlü ilişkinin tatmin olmadığını görüyorum. Ama nesnenin neden ayrıldığını anlamıyorum.
Gilgamesz

28
Lütfen yalnızca bağlantılarla cevap veren bir yanıt göndermeyin.
El Mac

6
Bu cevabın soru ile nasıl ilişkili olduğunu göremiyor musunuz?
Eugen Labun

270

Çözüm basit, sadece veya CascadeType.MERGEyerine kullanın .CascadeType.PERSISTCascadeType.ALL

Aynı sorunu yaşadım ve CascadeType.MERGEbenim için çalıştım.

Umarım sıralanırsın.


5
Şaşırtıcı bir şekilde benim için de çalıştı. CascadeType.ALL diğer tüm kaskad türlerini içerdiğinden hiçbir anlam ifade etmiyor ... WTF? Ben bahar 4.0.4, bahar veri jpa 1.8.0 ve hazırda bekleme 4.X var. Herkes ALL neden çalışmıyor, ancak MERGE çalışır herhangi bir düşüncesi var mı?
Vadim Kirilchuk

22
@VadimKirilchuk Bu benim için de çalıştı ve tam anlamıyla. İşlem PERSISTED olduğundan, PERSIST Hesabı için de çalışır ve Hesap zaten db'de olduğundan bu çalışmaz. Ancak CascadeType.MERGE ile Hesap bunun yerine otomatik olarak birleştirilir.
Gunslinger

4
Bu, işlem kullanmıyorsanız olabilir.
lanoxx

1
başka bir çözüm: zaten var olan nesneyi
eklememeye

3
Teşekkür ederim dostum. Referans anahtarının BOŞ OLMAMASI için kısıtlamanız varsa, kalıcı nesnenin eklenmesini önlemek mümkün değildir. Yani bu tek çözüm. Tekrar teşekkürler.
makkasi

22

Basamaklı varlığı alt kuruluştan kaldırınTransaction , yalnızca şöyle olmalıdır:

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

( FetchType.EAGERvarsayılan olarak kaldırılabilir @ManyToOne)

Bu kadar!

Neden? Alt varlıkta "basamakla TÜMÜ" diyerek Transaction, her DB işleminin üst varlığa yayılmasını gerektirir Account. Eğer yaparsan persist(transaction),persist(account) çağrılacaktır.

Ancak yalnızca geçici (yeni) varlıklara geçilebilir persist( Transactionbu durumda). Ayrılmış (veya diğer geçici olmayan durum) olanlar ( Accountbu durumda, zaten DB'de olduğu gibi ) olmayabilir .

Bu nedenle istisna elde edersiniz "müstakil varlık devam etmek için geçti" . AccountVarlık kastedilmektedir! TransactionAradığınız kişi değil persist.


Genellikle çocuktan ebeveyne yayılmak istemezsiniz. Ne yazık ki, kitaplarda (iyi olanlarda bile) ve internette tam olarak bunu yapan birçok kod örneği vardır. Bilmiyorum, neden ... Belki bazen fazla düşünmeden basitçe tekrar tekrar kopyaladım ...

Sanırım remove(transaction)o @ManyToOne içinde hala "TÜM basamakla" sahipseniz ne olur ? account(Btw, tüm diğer işlemler ile!) Yanı DB'den silinecektir. Ama niyetin bu değildi, değil mi?


Sadece eklemek istiyorum, eğer niyetiniz gerçekten çocuğu ebeveyn ile birlikte kurtarmak ve aynı zamanda ebeveyn ile birlikte çocuk (ebeveyn) ve adres (çocuk) gibi adresi ile birlikte DB tarafından otomatik olarak oluşturulan, daha sonra Kişiye kaydet'i çağırmadan önce, işlem yönteminizde adrese kaydetmek için bir çağrı. Bu şekilde DB tarafından oluşturulan id ile birlikte kaydedilir. Hibenate hala 2 sorgu yaptığı için performans üzerinde hiçbir etkisi yoktur, sadece sorguların sırasını değiştiriyoruz.
Vikky

Hiçbir şey iletemezsek, tüm durumlar için hangi varsayılan değeri alırız.
Dhwanil Patel

15

Birleştirme kullanmak riskli ve zorlayıcıdır, bu nedenle durumunuzda kirli bir çözümdür. En azından bir varlık nesnesini birleştirmek için ilettiğinizde, işleme eklenmeyi durdurduğunu ve bunun yerine şimdi ekli yeni bir varlığın döndürüldüğünü hatırlamanız gerekir . Bu, herhangi birinin eski varlık nesnesini hala elinde bulundurması durumunda, nesnede yapılan değişiklikler sessizce göz ardı edilir ve taahhütte atılır.

Kodun tamamını burada göstermiyorsunuz, bu nedenle işlem deseninizi iki kez kontrol edemiyorum. Böyle bir duruma ulaşmanın bir yolu, birleştirme işlemini gerçekleştirirken ve devam ederken etkin bir işleminiz yoksa. Bu durumda, kalıcılık sağlayıcısının gerçekleştirdiğiniz her JPA işlemi için yeni bir işlem açması ve çağrı geri dönmeden hemen önce işlem yapması ve kapatması beklenir. Bu durumda, birleştirme ilk işlemde çalıştırılır ve birleştirme yöntemi döndükten sonra işlem tamamlanır ve kapatılır ve döndürülen varlık şimdi ayrılır. Altındaki kalıcılık, daha sonra ikinci bir işlem açacak ve ayrılan bir varlığa başvurmaya çalışarak bir istisna verecektir. Ne yaptığınızı çok iyi bilmiyorsanız, kodunuzu her zaman bir işlemin içine sarın.

Kapsayıcı tarafından yönetilen işlem kullanıldığında böyle bir şey olur. Not: Bu, yöntemin bir oturum çekirdeğinin içinde olduğunu ve Yerel veya Uzak arabirim yoluyla çağrıldığını varsayar.

@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);
}

Aynı sorunla karşı karşıyayım., @Transaction(readonly=false)Hizmet katmanında kullandım., Hala aynı sorunu alıyorum,
Senthil Arumugam SP

İşlerin neden bu şekilde çalıştığını tam olarak anladığımı söyleyemem, ancak kalıcı bir yöntem ve bir Varlık eşlemesine görüntülemeyi İşlemsel bir ek açıklama içine yerleştirerek sorunumu düzelttim, teşekkürler.
Deadron

Bu aslında en çok öne sürülenden daha iyi bir çözümdür.
Aleksei Maide

13

Muhtemelen bu durumda accountbirleştirme mantığını kullanarak nesnenizi elde persistetmişsinizdir ve yeni nesneleri sürdürmek için kullanılır ve hiyerarşinin zaten var olan bir nesneye sahip olup olmadığından şikayet eder. Bunun saveOrUpdateyerine bu gibi durumlarda kullanmalısınız persist.


3
JPA, bu yüzden benzer yöntem .merge () olduğunu düşünüyorum, ama bu bana aynı istisnayı verir. Açık olmak gerekirse, İşlem yeni bir nesnedir, Hesap değildir.
Paul Sanwald

Kullanılması @PaulSanwald mergeüzerinde transactionAynı hatayı alırsanız nesne?
dan

aslında, hayır, yanlış konuştum. eğer .merge (işlem), o zaman işlem hiç devam etmez.
Paul Sanwald

@PaulSanwald Hmm, transactionbunun devam etmediğinden emin misin ? Nasıl kontrol ettin? mergeKalıcı nesneye bir başvuru döndürdüğünü unutmayın .
dan

.merge () tarafından döndürülen nesnenin boş bir kimliği vardır. Ayrıca, daha sonra bir .findAll () yapıyorum ve benim nesne orada değil.
Paul Sanwald

12

Persist yöntemine id (pk) iletmeyin veya persist () yerine save () yöntemini denemeyin.


2
İyi tavsiye! Ancak yalnızca id üretilirse. Atanmışsa, kimliği ayarlamak normaldir.
Aldian

bu benim için çalıştı. Ayrıca, TestEntityManager.persistAndFlush()bir örneği yönetilen ve kalıcı hale getirmek ve ardından kalıcılık bağlamını temel alınan veritabanıyla senkronize etmek için kullanabilirsiniz. Orijinal kaynak varlığı döndürür
Anyul Rivas

7

Bu çok yaygın bir soru olduğundan, bu cevabın dayandığı bu makaleyi yazdım .

Sorunu çözmek için şu adımları uygulamanız gerekir:

1. Alt ilişkilendirme kaldırılıyor

Yani, kaldırmak gerekir @CascadeType.ALLdan @ManyToOnedernek. Alt varlıklar ebeveyn derneklerine geçiş yapmamalıdır. Yalnızca ebeveyn varlıklar çocuk varlıklar için basamaklandırılmalıdır.

@ManyToOne(fetch= FetchType.LAZY)

fetchÖzelliği ayarladığım için dikkat edin FetchType.LAZYçünkü istekli getirme performans için çok kötü .

2. Birliğin iki tarafını da ayarlayın

İki yönlü bir ilişkilendirmeniz olduğunda, üst varlıktaki her iki tarafı addChildve removeChildyöntemleri kullanarak senkronize etmeniz gerekir :

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

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

İki yönlü bir ilişkilendirmenin her iki ucunu da senkronize etmenin neden önemli olduğu hakkında daha fazla bilgi için bu makaleye göz atın .


4

Senin varlık tanımı içinde belirterek değiliz @JoinColumn için Accountbir birleştirilebilir Transaction. Bunun gibi bir şey isteyeceksiniz:

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

DÜZENLEME: Şey, sanırım @Tablesınıfınızda ek açıklama kullanıyorsanız bu yararlı olurdu . Heh. :)


2
evet bu olduğunu sanmıyorum, hepsi aynı, @JoinColumn (name = "fromAccount_id", başvurulanColumnName = "id") ekledim ve işe yaramadı :).
Paul Sanwald

Evet, genellikle varlıkları tablolara eşlemek için bir eşleme xml dosyası kullanmıyorum, bu yüzden genellikle ek açıklama tabanlı olduğunu varsayıyorum. Ama tahmin etmem gerekirse, varlıkları tablolarla eşleştirmek için bir hibernate.xml kullanıyorsunuz, değil mi?
NemesisX00

hayır, JPA yay verileri kullanıyorum, bu yüzden hepsi ek açıklama tabanlı. Diğer tarafta bir "mappedBy" ek açıklaması var: @OneToMany (cascade = {CascadeType.ALL}, fetch = FetchType.EAGER, mappedBy = "fromAccount")
Paul Sanwald 13:12

4

Ek açıklamalarınızın bire çok ilişkisini düzgün bir şekilde yönettiği bildirilse bile bu kesin istisna ile karşılaşabilirsiniz. Yeni bir alt nesne eklerken, Transaction, ekli bir veri modeline birincil anahtar değerini yönetmek gerekir - Eğer gerekmiyor sürece . Aramadan önce aşağıdaki gibi bildirilen bir alt varlık için birincil anahtar değeri sağlarsanız persist(T), bu istisna ile karşılaşırsınız.

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

Bu durumda ek açıklamalar, veritabanının ekleme sonrasında varlığın birincil anahtar değerlerinin oluşturulmasını yöneteceğini bildirir. Kendinize (örneğin Kimlik ayarlayıcı aracılığıyla) sağlamak bu istisnayı doğurur.

Alternatif olarak, ancak etkili bir şekilde aynı, bu ek açıklama bildirimi aynı istisna ile sonuçlanır:

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

Bu nedenle, idzaten yönetiliyorken uygulama kodunuzdaki değeri ayarlamayın .


4

Hiçbir şey yardımcı olmazsa ve hala bu istisnayı alıyorsanız, equals()yöntemlerinizi gözden geçirin ve buna çocuk koleksiyonu eklemeyin. Özellikle gömülü koleksiyonların derin yapısına sahipseniz (örneğin A, Bs içerir, B C içerir, vb.).

Örnek 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!
    }
  }

Yukarıdaki örnekte, equals()çeklerden işlemleri kaldırın . Bunun nedeni, hazırda bekletme modunun eski nesneyi güncellemeye çalışmadığınızı ima etmesi, ancak alt koleksiyondaki öğeyi her değiştirdiğinizde kalıcı olarak yeni bir nesne geçirmenizdir.
Tabii ki bu çözümler tüm uygulamalara uymayacak ve equalsve hashCodeyöntemlerine dahil etmek istediğiniz şeyi dikkatlice tasarlamalısınız .


1

Belki OpenJPA'nın hatası, Geri alma işlemi @Version alanını sıfırlar, ancak pcVersionInit doğru kalır. @Version alanını bildiren bir AbstraceEntity var. PcVersionInit alanını sıfırlayarak bu sorunu çözebilirim. Ama bu iyi bir fikir değil. Bence kademeli varlık var olduğunda işe yaramaz.

    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;
            }

1

Her Hesap için İşlem ayarlamanız gerekir.

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

Veya kimlikleri birçok tarafta null değerine ayarlamak için (uygunsa) yeterli olabilir.

// 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.

1
cascadeType.MERGE,fetch= FetchType.LAZY

1
Merhaba James ve hoş geldiniz, sadece kod cevapları denemek ve kaçınmalısınız. Lütfen bunun soruda belirtilen sorunu nasıl çözdüğünü belirtin (ve uygulanabilir olduğunda veya uygulanmadığında, API düzeyi vb.).
Maarten Bodewes

VLQ inceleyenleri: bkz. Meta.stackoverflow.com/questions/260411/… . sadece kod yanıtları silmeyi hak etmez, uygun eylem "Tamam görünüyor" u seçmektir.
Nathan Hughes


0

Benim durumumda ben işlem yapılıyor edildi devam etmesi yöntem kullanılmıştır. Değişen Açık inat etmek kurtarmak yöntemi, bu giderilmiş var.


0

Yukarıdaki çözümler yalnızca bir kez işe yaramazsa, varlık sınıfının alıcı ve ayarlayıcı yöntemlerini yorumlayın ve id değerini ayarlamayın. (Birincil anahtar) O zaman bu işe yarayacaktır.


0

@OneToMany (mappedBy = "xxxx", cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE}) benim için çalıştı.


0

Bağımlı nesneyi bir sonrakinden önce kaydederek çözüldü.

Bu benim başıma geldi çünkü ID ayarlamıyordum (otomatik olarak oluşturulmadı). ve @ManytoOne ilişkisi ile kurtarmaya çalışıyor

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.