it-swarm.com.de

JPA/Hibernate: Freistehende Entität übergeben, um persistent zu bleiben

Ich habe ein JPA-beständiges Objektmodell, das eine Viele-zu-Eins-Beziehung enthält: Ein Konto hat viele Transaktionen. Eine Transaktion hat ein Konto.

Hier ist ein Ausschnitt des Codes:

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

Ich kann ein Kontoobjekt erstellen, Transaktionen hinzufügen und das Kontoobjekt korrekt beibehalten. Wenn ich jedoch eine Transaktion erstelle, mit einem vorhandenen, bereits bestehenden Konto und dem der Transaktion -, erhalte ich eine Ausnahme:

Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.paulsanwald.Account
    at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.Java:141) 

Daher kann ich ein Konto beibehalten, das Transaktionen enthält, nicht jedoch eine Transaktion, die ein Konto enthält. Ich dachte, das liegt daran, dass das Konto möglicherweise nicht angehängt ist, aber dieser Code gibt mir immer noch die gleiche Ausnahme:

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

Wie kann ich eine Transaktion, die einem bereits persistenten Account-Objekt zugeordnet ist, richtig speichern?

164
Paul Sanwald

Dies ist ein typisches Problem der bidirektionalen Konsistenz. Es wird in this link sowie this link gut diskutiert.

Wie in den Artikeln in den vorherigen 2 Links beschrieben, müssen Sie Ihre Setter auf beiden Seiten der bidirektionalen Beziehung fixieren. Ein Beispiel für die Eins-Seite ist in this link.

Ein Beispiel für die Many-Seite ist in this link.

Nachdem Sie Ihre Setter korrigiert haben, möchten Sie den Zugriffstyp "Entität" als "Eigenschaft" festlegen. Um den Zugriffstyp "Eigenschaft" zu deklarieren, empfiehlt es sich, ALLE Annotationen aus den Member-Eigenschaften in die entsprechenden Getter zu verschieben. Ein großes Wort der Vorsicht ist, die Zugriffstypen "Field" und "Property" nicht innerhalb der Entitätsklasse zu mischen. Andernfalls ist das Verhalten durch die JSR-317-Spezifikationen undefiniert.

104
Sym-Sym

Die Lösung ist einfach, verwenden Sie einfach den CascadeType.MERGE anstelle von CascadeType.PERSIST oder CascadeType.ALL

Ich hatte das gleiche Problem und CascadeType.MERGE hat für mich gearbeitet.

Ich hoffe du bist sortiert.

198
ochiWlad

Die Verwendung der Zusammenführung ist riskant und knifflig, daher ist dies in Ihrem Fall eine schmutzige Problemumgehung. Sie müssen mindestens daran denken, dass Sie, wenn Sie ein Entitätsobjekt zum Zusammenführen übergeben, stoppt an die Transaktion angehängt werden und stattdessen eine neue, jetzt angefügte Entität zurückgegeben wird. Das heißt, wenn jemand das alte Objekt noch in seinem Besitz hat, werden Änderungen daran ignoriert und beim Festschreiben weggeworfen.

Sie zeigen hier nicht den vollständigen Code an, daher kann ich Ihr Transaktionsmuster nicht noch einmal überprüfen. Eine Möglichkeit, zu einer solchen Situation zu gelangen, besteht darin, wenn Sie beim Ausführen der Zusammenführung keine Transaktion aktiv haben und bestehen bleiben. In diesem Fall wird von einem Persistenzanbieter erwartet, dass er für jede von Ihnen durchgeführte JPA-Operation eine neue Transaktion öffnet und diese sofort festschreibt und schließt, bevor der Anruf zurückkehrt. Wenn dies der Fall ist, wird die Zusammenführung in einer ersten Transaktion ausgeführt. Nach der Rückkehr der Zusammenführungsmethode wird die Transaktion abgeschlossen und abgeschlossen, und die zurückgegebene Entität wird nun getrennt. Das Persist darunter würde dann eine zweite Transaktion öffnen und versuchen, sich auf eine Entität zu beziehen, die getrennt wurde, wobei eine Ausnahme gegeben wird. Packen Sie Ihren Code immer in eine Transaktion ein, es sei denn, Sie wissen sehr genau, was Sie tun.

Bei einer durch Container verwalteten Transaktion würde es ungefähr so ​​aussehen. Hinweis: Dies setzt voraus, dass sich die Methode in einem Session-Bean befindet und über die Local- oder Remote-Schnittstelle aufgerufen wird.

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

Wahrscheinlich haben Sie in diesem Fall Ihr account-Objekt unter Verwendung der Zusammenführungslogik erhalten, und persist wird verwendet, um neue Objekte zu persistieren, und es beschwert sich, wenn die Hierarchie ein bereits vorhandenes Objekt enthält. Sie sollten in solchen Fällen saveOrUpdate anstelle von persist verwenden.

9
dan

Übergeben Sie keine id (pk) an die persist-Methode oder versuchen Sie die save () - Methode anstelle von persist ().

7
Angad Bansode

Wenn nichts hilft und Sie immer noch diese Ausnahme erhalten, überprüfen Sie Ihre equals()-Methoden - und fügen Sie keine untergeordneten Auflistungen hinzu. Insbesondere, wenn Sie eine tiefe Struktur eingebetteter Sammlungen haben (z. B. enthält A Bs, B enthält Cs usw.).

Im Beispiel von 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!
    }
  }

In obigem Beispiel entfernen Sie Transaktionen von equals()-Prüfungen. Der Grund dafür ist, dass der Ruhezustand impliziert, dass Sie nicht versuchen, das alte Objekt zu aktualisieren, sondern ein neues Objekt übergeben, das beibehalten wird, wenn Sie das Element der untergeordneten Auflistung ändern.
Natürlich passen diese Lösungen nicht für alle Anwendungen, und Sie sollten sorgfältig entwerfen, was Sie in die Methoden equals und hashCode einschließen möchten.

4
FazoM

In Ihrer Entitätsdefinition geben Sie nicht das @JoinColumn für die Account an, die mit einer Transaction verbunden ist. Sie wollen so etwas wie:

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

EDIT: Nun, ich denke, das wäre nützlich, wenn Sie die @Table-Annotation für Ihre Klasse verwenden würden. Heh. :)

4
NemesisX00

Sie müssen für jedes Konto eine Transaktion festlegen.

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

Oder es reicht aus (falls zutreffend), um IDs auf vielen Seiten auf Null zu setzen.

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

Zunächst einmal vielen Dank für eine sehr gute Beschreibung und Demonstration des Problems.

Betrachten Sie eine einfache Lösung: Entfernen Sie die Kaskadierung aus der untergeordneten Entität Transaction, es sollte nur Folgendes sein:

@ManyToOne // no cascading here!
private Account fromAccount;

(FetchType.EAGER kann entfernt werden, da es die Standardeinstellung für @ManyToOne ist)

Das ist alles!

Warum? Wenn Sie für die untergeordnete Entität "kaskadieren" angeben, müssen Sie jede DB-Operation an die übergeordnete Entität weitergeben. Wenn Sie dann persist(transaction) ausführen, wird auch persist(account) aufgerufen. Es können jedoch nur vorübergehende (neue) Entitäten an persist () übergeben werden, die abgelösten (transaction ist in diesem Fall bereits in der Datenbank) möglicherweise nicht. Daher erhalten Sie die Ausnahme "Getrenntes Objekt, das an persist übergeben wird"

Im Allgemeinen möchten Sie sich nicht von einem Kind auf ein Elternteil ausbreiten. Leider gibt es viele Codebeispiele in Büchern (auch in guten) und im Netz, die genau das tun. Ich weiß nicht, warum ... Vielleicht manchmal einfach immer und immer wieder kopiert, ohne viel darüber nachzudenken ... Was passiert, wenn Sie remove(transaction) immer noch "Kaskade ALL" in diesem @ManyToOne? Die account (übrigens bei allen anderen Transaktionen) wird ebenfalls aus der Datenbank gelöscht. Aber das war nicht deine Absicht, oder?

1
Eugen Labun

Möglicherweise handelt es sich um einen Fehler von OpenJPA. Wenn das Rollback zurückgesetzt wird, wird das @Version-Feld zurückgesetzt. Ich habe eine AbstraceEntity, die das @Version-Feld deklariert hat. Ich kann das Problem umgehen, indem Sie das Feld pcVersionInit zurücksetzen. Aber es ist keine gute Idee. Ich denke, dass es nicht funktioniert, wenn Kaskade persistieren.

    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
Yaocl

Auch wenn Ihre Anmerkungen korrekt deklariert sind, um die Eins-zu-Viele-Beziehung richtig zu verwalten, kann diese genaue Ausnahme immer noch auftreten. Wenn Sie ein neues untergeordnetes Objekt, Transaction, zu einem angefügten Datenmodell hinzufügen, müssen Sie den Primärschlüsselwert - verwalten, sofern Sie nicht - benötigen. Wenn Sie einen Primärschlüsselwert für eine untergeordnete Entität angeben, die vor dem Aufruf von persist(T) wie folgt deklariert wurde, wird diese Ausnahme angezeigt.

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

In diesem Fall erklären die Anmerkungen, dass die Datenbank die Erzeugung der Primärschlüsselwerte der Entität beim Einfügen verwaltet. Wenn Sie selbst einen Wert angeben (z. B. über den Setter der Id), führt dies zu dieser Ausnahme.

Alternativ, aber im Grunde dasselbe, führt diese Annotationsdeklaration zu derselben Ausnahme:

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

Setzen Sie also nicht den Wert id in Ihrem Anwendungscode, wenn dieser bereits verwaltet wird.

1
dan

Wenn die obigen Lösungen nicht nur einmal funktionieren, kommentieren Sie die Getter- und Setter-Methoden der Entity-Klasse und setzen Sie nicht den Wert von id. (Primärschlüssel) Dann funktioniert dies.

0
Shubham
cascadeType.MERGE,fetch= FetchType.LAZY
0
James Siva

In meinem Fall habe ich eine Transaktion durchgeführt, als persist method verwendet wurde. Beim Ändern von persist to save wurde das Problem behoben.

0
H.Ostwal