PersistentObjectException: odłączony byt przekazany do trwałego wyrzucenia przez JPA i Hibernację

237

Mam model obiektów utrwalony przez JPA, który zawiera relację wiele do jednego: Accountma wiele Transactions. A Transactionma jeden Account.

Oto fragment kodu:

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

Jestem w stanie utworzyć Accountobiekt, dodać do niego transakcje i Accountpoprawnie utrwalić obiekt. Ale kiedy tworzę transakcję, używając istniejącego już utrwalonego Konta i utrwalając Transakcję , otrzymuję wyjątek:

Przyczyna: org.hibernate.PersistentObjectException: odłączony byt przekazany do trwałego: com.paulsanwald.Account at org.hibernate.event.internal.DefaultPersistEventListener.onPersist (DefaultPersistEventListener.java:141)

Tak więc jestem w stanie utrzymać Accounttransakcję zawierającą transakcje, ale nie transakcję zawierającą transakcję Account. Myślałem, że dzieje się tak, ponieważ Accountmoże nie być dołączony, ale ten kod wciąż daje mi ten sam wyjątek:

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

Jak mogę poprawnie zapisać obiekt Transactionpowiązany z już utrwalonym Accountobiektem?

Paul Sanwald
źródło
15
W moim przypadku ustawiałem identyfikator encji, którą próbowałem utrwalić za pomocą Entity Manager. Kiedy usunąłem seter dla identyfikatora, zaczął działać dobrze.
Rushi Shah
W moim przypadku nie ustawiałem identyfikatora, ale dwóch użytkowników korzystało z tego samego konta, jeden z nich utrwalił byt (poprawnie), a błąd pojawił się, gdy drugi próbował utrzymać ten sam byt, który już był trwało.
sergioFC

Odpowiedzi:

129

Jest to typowy problem z dwukierunkową spójnością. Jest dobrze omówione w tym linku, a także w tym linku.

Zgodnie z artykułami w poprzednich 2 linkach musisz naprawić seterów po obu stronach relacji dwukierunkowej. Przykładowy setter dla jednej strony znajduje się w tym łączu.

Przykładowy setter dla strony Many znajduje się w tym łączu.

Po poprawieniu ustawień należy zadeklarować, że typ dostępu encji to „Właściwość”. Najlepszą praktyką do deklarowania typu dostępu „Właściwość” jest przeniesienie WSZYSTKICH adnotacji z właściwości elementu do odpowiednich modułów pobierających. Dużym ostrzeżeniem jest, aby nie mieszać typów dostępu „Pole” i „Właściwość” w klasie encji, w przeciwnym razie zachowanie nie jest określone w specyfikacji JSR-317.

Sym-Sym
źródło
2
ps: adnotacja @Id to taka, której hibernacja używa do identyfikacji typu dostępu.
Diego Plentz,
2
Wyjątkiem jest: detached entity passed to persistdlaczego poprawa spójności sprawia, że ​​działa? Ok, spójność została naprawiona, ale obiekt jest nadal odłączony.
Gilgamesz
1
@Sam, wielkie dzięki za wyjaśnienie. Ale nadal nie rozumiem. Widzę, że bez „specjalnego” setera związek dwukierunkowy nie jest usatysfakcjonowany. Ale nie rozumiem, dlaczego obiekt został odłączony.
Gilgamesz
28
Proszę nie zamieszczać odpowiedzi, która zawiera wyłącznie linki.
El Mac
6
Nie rozumiesz w ogóle, jak ta odpowiedź jest związana z pytaniem?
Eugen Labun,
270

Rozwiązanie jest proste, wystarczy użyć CascadeType.MERGEzamiast CascadeType.PERSISTlub CascadeType.ALL.

Miałem ten sam problem i CascadeType.MERGEpracował dla mnie.

Mam nadzieję, że jesteś posortowany.

ochiWlad
źródło
5
Zaskakujące, że to też dla mnie zadziałało. To nie ma sensu, ponieważ CascadeType.ALL obejmuje wszystkie inne typy kaskad ... WTF? Mam wiosnę 4.0.4, wiosenne dane jpa 1.8.0 i hibernację 4.X .. Czy ktoś ma jakieś przemyślenia, dlaczego WSZYSTKO nie działa, ale MERGE działa?
Vadim Kirilchuk
22
@VadimKirilchuk To też zadziałało dla mnie i ma sens. Ponieważ transakcja jest PERSISTED, próbuje ona również PERSIST do konta, a to nie działa, ponieważ konto jest już w bazie danych. Jednak dzięki CascadeType.MERGE konto jest automatycznie scalane.
Gunslinger,
4
Może się to zdarzyć, jeśli nie korzystasz z transakcji.
lanoxx,
1
inne rozwiązanie: staraj się nie wstawiać już utrwalonego obiektu :)
Andrii Plotnikov
3
Dziękuje. Nie można uniknąć wstawienia utrwalonego obiektu, jeśli istnieje ograniczenie, że klucz referencyjny ma wartość NULL. To jest jedyne rozwiązanie. Jeszcze raz dziękuję.
makkasi
22

Usuń kaskadę z elementu potomnegoTransaction , powinno to być po prostu:

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

( FetchType.EAGERmożna usunąć, jak to domyślnie dla @ManyToOne)

To wszystko!

Czemu? Mówiąc „kaskadowo WSZYSTKO” na encji podrzędnej, wymagasz Transaction, aby każda operacja DB była propagowana do encji nadrzędnej Account. Jeśli to zrobisz persist(transaction), persist(account)zostanie również wywołane.

Ale tylko przejściowe (nowe) byty mogą być przekazywane persist( Transactionw tym przypadku). Odłączone (lub inne nieprzemijające stany) mogą nie ( Accountw tym przypadku, ponieważ są już w DB).

Dlatego otrzymujesz wyjątek „odłączony byt przekazany do trwałego” . AccountJednostka rozumie! Nie ten Transaction, do którego dzwonisz persist.


Na ogół nie chcesz rozmnażać się z dziecka na rodzica. Niestety istnieje wiele przykładów kodu w książkach (nawet w dobrych) i przez sieć, które właśnie to robią. Nie wiem, dlaczego ... Może czasami po prostu kopiowałem w kółko bez większego zastanowienia ...

Zgadnij, co się stanie, jeśli remove(transaction)nadal będziesz mówić „kaskadowo WSZYSTKO” w tym @ManyToOne? account(Btw, wszystkie inne transakcje!) Również zostaną usunięte z bazy danych. Ale to nie był twój cel, prawda?

Eugen Labun
źródło
Chcę tylko dodać, jeśli naprawdę chcesz uratować dziecko wraz z rodzicem, a także usunąć rodzica wraz z dzieckiem, na przykład osobę (rodzic) i adres (dziecko) wraz z adresem i identyfikatorem automatycznie generowanym przez DB, a następnie przed wywołaniem funkcji Zapisz na osobie, po prostu wykonaj wezwanie do zapisania adresu w metodzie transakcji. W ten sposób zostanie zapisany wraz z identyfikatorem wygenerowanym również przez DB. Bez wpływu na wydajność, ponieważ Hibenate wciąż wykonuje 2 zapytania, zmieniamy tylko kolejność zapytań.
Vikky
Jeśli nie możemy niczego przekazać, to jaką domyślną wartość przyjmuje dla wszystkich przypadków.
Dhwanil Patel
15

Korzystanie ze scalania jest ryzykowne i trudne, więc w twoim przypadku jest to nieprzyjemne obejście. Musisz przynajmniej pamiętać, że po przekazaniu obiektu encji do scalenia przestaje on być dołączany do transakcji, a zamiast tego zwracany jest nowy, teraz dołączony byt. Oznacza to, że jeśli ktoś nadal ma w posiadaniu stary obiekt bytu, zmiany w nim są po cichu ignorowane i wyrzucane po zatwierdzeniu.

Nie wyświetlasz tutaj pełnego kodu, więc nie mogę dokładnie sprawdzić wzorca transakcji. Jednym ze sposobów na osiągnięcie takiej sytuacji jest to, że podczas wykonywania scalania i utrzymywania transakcji nie jest aktywna. W takim przypadku oczekuje się, że dostawca trwałości otworzy nową transakcję dla każdej wykonywanej operacji JPA oraz natychmiast zatwierdzi i zamknie ją przed powrotem połączenia. W takim przypadku scalanie zostanie uruchomione w pierwszej transakcji, a następnie po zwróceniu metody scalania transakcja zostanie zakończona i zamknięta, a zwrócona jednostka zostanie odłączona. Utrwalenie poniżej otworzy następnie drugą transakcję i spróbuje odwołać się do jednostki, która jest odłączona, dając wyjątek. Zawsze zawijaj kod w transakcji, chyba że dobrze wiesz, co robisz.

Korzystając z transakcji zarządzanej przez kontener, wyglądałoby to tak. Uwaga: zakłada się, że metoda znajduje się w komponencie bean sesji i jest wywoływana przez interfejs lokalny lub zdalny.

@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
źródło
Mam do czynienia z tym samym problemem. Korzystałem z @Transaction(readonly=false)warstwy usług. Nadal otrzymuję ten sam problem,
Senthil Arumugam SP
Nie mogę powiedzieć, że w pełni rozumiem, dlaczego wszystko działa w ten sposób, ale umieszczenie metody persist i widok mapowania encji razem w adnotacji transakcyjnej naprawił mój problem, więc dzięki.
Deadron
To jest rzeczywiście lepsze rozwiązanie niż najbardziej uprzywilejowane.
Aleksei Maide
13

Prawdopodobnie w tym przypadku accountobiekt został uzyskany przy użyciu logiki scalania i persistsłuży do utrwalania nowych obiektów i będzie narzekać, jeśli hierarchia ma już utrwalony obiekt. W saveOrUpdatetakich przypadkach powinieneś użyć zamiast persist.

dan
źródło
3
jest to JPA, więc myślę, że analogiczną metodą jest .merge (), ale daje to ten sam wyjątek. Dla jasności, Transakcja to nowy obiekt, Konto nie.
Paul Sanwald
@PaulSanwald Używając mergena transactionobiekcie otrzymujesz ten sam błąd?
dan
właściwie nie, źle powiedziałem. jeśli I .merge (transakcja), wówczas transakcja w ogóle się nie utrwala.
Paul Sanwald
@PaulSanwald Hmm, czy jesteś pewien, że transactionto nie przetrwało? Jak sprawdziłeś? Zauważ, że mergezwraca odwołanie do utrwalonego obiektu.
dan
obiekt zwrócony przez .merge () ma zerowy identyfikator. ponadto wykonuję później funkcję .findAll (), a mojego obiektu nie ma.
Paul Sanwald
12

Nie przekazuj id (pk), aby utrwalić metodę, ani spróbuj metody save () zamiast persist ().

Angad Bansode
źródło
2
Dobra rada! Ale tylko wtedy, gdy wygenerowany zostanie identyfikator. W przypadku przypisania jest normalnym ustawieniem identyfikatora.
Aldian
to działało dla mnie. Można także użyć tego TestEntityManager.persistAndFlush(), aby instancja była zarządzana i trwała, a następnie zsynchronizować kontekst trwałości z bazową bazą danych. Zwraca oryginalną jednostkę źródłową
Anyul Rivas
7

Ponieważ jest to bardzo częste pytanie, napisałem ten artykuł , na którym opiera się ta odpowiedź.

Aby rozwiązać problem, wykonaj następujące kroki:

1. Usuwanie kaskadowego powiązania dzieci

Tak, trzeba usunąć @CascadeType.ALLze @ManyToOnestowarzyszenia. Podmioty potomne nie powinny kaskadować do stowarzyszeń nadrzędnych. Tylko jednostki nadrzędne powinny być kaskadowane do jednostek podrzędnych.

@ManyToOne(fetch= FetchType.LAZY)

Zauważ, że ustawiłem ten fetchatrybut na, FetchType.LAZYponieważ chętne pobieranie jest bardzo niekorzystne dla wydajności .

2. Ustaw obie strony stowarzyszenia

Ilekroć masz powiązanie dwukierunkowe, musisz zsynchronizować obie strony za pomocą addChildi removeChildmetod w encji nadrzędnej:

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

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

Aby uzyskać więcej informacji o tym, dlaczego ważna jest synchronizacja obu końców powiązania dwukierunkowego, zapoznaj się z tym artykułem .

Vlad Mihalcea
źródło
4

W definicji encji nie określasz @JoinColumn dla Accountprzyłączonego do a Transaction. Będziesz chciał coś takiego:

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

EDYCJA: Cóż, myślę, że byłoby to przydatne, gdybyś używał @Tableadnotacji na swojej klasie. Heh :)

NemesisX00
źródło
2
tak, nie sądzę, że to jest to samo, dodałem @JoinColumn (name = "fromAccount_id", referencedColumnName = "id") i to nie działało :).
Paul Sanwald
Tak, zwykle nie używam pliku XML mapowania do mapowania jednostek na tabele, więc zazwyczaj zakładam, że jest oparty na adnotacjach. Ale gdybym musiał zgadywać, używasz pliku hibernacji.xml do mapowania encji na tabele, prawda?
NemesisX00,
nie, używam wiosennych danych JPA, więc wszystko opiera się na adnotacjach. Z drugiej strony mam adnotację „mappedBy”: @OneToMany (cascade = {CascadeType.ALL}, fetch = FetchType.EAGER, mappedBy = "fromAccount")
Paul Sanwald
4

Nawet jeśli adnotacje są zadeklarowane poprawnie, aby właściwie zarządzać relacją jeden do wielu, nadal możesz napotkać ten precyzyjny wyjątek. Podczas dodawania nowego obiektu podrzędnego Transactiondo dołączonego modelu danych należy zarządzać wartością klucza podstawowego - chyba że nie należy tego robić . Jeśli podasz wartość klucza podstawowego dla encji podrzędnej zadeklarowanej w następujący sposób przed wywołaniem persist(T), napotkasz ten wyjątek.

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

W tym przypadku adnotacje deklarują, że baza danych będzie zarządzać generowaniem wartości klucza podstawowego jednostki po wstawieniu. Podanie jednego (na przykład za pomocą narzędzia do ustawiania identyfikatora) powoduje ten wyjątek.

Alternatywnie, ale faktycznie tak samo, ta deklaracja adnotacji powoduje ten sam wyjątek:

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

Dlatego nie ustawiaj idwartości w kodzie aplikacji, gdy jest ona już zarządzana.

dan
źródło
4

Jeśli nic nie pomaga i nadal występuje ten wyjątek, przejrzyj swoje equals()metody - i nie uwzględniaj w nim kolekcji podrzędnej. Zwłaszcza jeśli masz głęboką strukturę osadzonych kolekcji (np. A zawiera Bs, B zawiera Cs itp.).

Na przykład 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!
    }
  }

W powyższym przykładzie usuń transakcje z equals()czeków. Wynika to z faktu, że hibernacja oznacza, że ​​nie próbujesz aktualizować starego obiektu, ale przekazujesz nowy obiekt, aby zachować go przy każdej zmianie elementu w kolekcji potomnej.
Oczywiście te rozwiązania nie będą pasować do wszystkich aplikacji i należy dokładnie zaprojektować to, co chcesz uwzględnić w metodach equalsi hashCode.

FazoM
źródło
1

Być może jest to błąd OpenJPA. Po przywróceniu resetuje pole @Version, ale pcVersionInit pozostaje prawdą. Mam AbstraceEntity, który zadeklarował pole @Version. Mogę to obejść, resetując pole pcVersionInit. Ale to nie jest dobry pomysł. Myślę, że to nie działa, gdy ma się element utrzymujący kaskadę.

    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
źródło
1

Musisz ustawić transakcję dla każdego konta.

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

Lub może wystarczyć (jeśli to konieczne), aby ustawić identyfikatory na null z wielu stron.

// 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
źródło
1
cascadeType.MERGE,fetch= FetchType.LAZY
James Siva
źródło
1
Cześć James i witaj, powinieneś unikać odpowiedzi tylko na kod. Proszę wskazać, w jaki sposób rozwiązuje to problem wskazany w pytaniu (i kiedy ma to zastosowanie, czy nie, poziom API itp.).
Maarten Bodewes
Recenzenci VLQ: patrz meta.stackoverflow.com/questions/260411/… . odpowiedzi tylko na kod nie zasługują na usunięcie, odpowiednią czynnością jest wybranie „Wygląda OK”.
Nathan Hughes
1

Moja odpowiedź oparta na JPA z Spring Data: po prostu dodałem @Transactionaladnotację do mojej zewnętrznej metody.

Dlaczego to działa?

Podmiot potomny natychmiast się odłączał, ponieważ nie istniał aktywny kontekst sesji hibernacji. Podanie transakcji wiosennej (dane JPA) zapewnia obecność sesji hibernacji.

Odniesienie:

https://vladmihalcea.com/a-beginners-guide-to-jpa-hibernate-entity-state-transitions/

JJ Zabkar
źródło
0

W moim przypadku popełniałem transakcję, gdy użyto metody persist . Po zmianie metody keep to save problem został rozwiązany.

H.Ostwal
źródło
0

Jeśli powyższe rozwiązania nie działają, wystarczy raz skomentować metody pobierające i ustawiające klasy encji i nie ustawiać wartości id. (Klucz podstawowy). To zadziała.

Shubham
źródło
0

@OneToMany (mappedBy = "xxxx", cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE}) działało dla mnie.

SateeshKasaboina
źródło
0

Rozwiązane przez zapisanie obiektu zależnego przed następnym.

Stało się tak, ponieważ nie ustawiałem identyfikatora (który nie został wygenerowany automatycznie). i próba zapisania w relacji @ManytoOne

Shahid Hussain Abbasi
źródło