JPA/Hibernate: отдельное лицо передано для сохранения

У меня есть сохраненная в JPA объектная модель, которая содержит отношение многие-к-одному: на Account много Transactions. Transaction имеет одну Account.

Вот фрагмент кода:

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

Я могу создать объект Account, добавить к нему транзакции и правильно сохранить объект Account. Но когда я создаю транзакцию, используя существующую уже сохраненную учетную запись и сохраняя транзакцию, я получаю исключение:

Вызывается: org.hibernate.PersistentObjectException: отсоединенная сущность передана для сохранения: com.paulsanwald.Account at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:141)

Итак, я могу сохранить Account которая содержит транзакции, но не транзакцию, которая имеет Account. Я думал, что это потому, что Account не может быть присоединена, но этот код все еще дает мне то же исключение:

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

Как правильно сохранить Transaction, связанную с уже сохраненным объектом Account?

Ответ 1

Это типичная проблема двунаправленной согласованности. Это хорошо обсуждается в этой ссылке, а также в этой ссылке.

В соответствии с статьями в предыдущих двух ссылках вам нужно исправить ваши сеттеры по обе стороны от двунаправленного отношения. В этой ссылке указан пример настройки для одной стороны .

В этой ссылке показан пример setter для стороны Many .

После того, как вы исправите свои сеттеры, вы хотите объявить тип доступа Entity "Свойством". Лучшей практикой объявления типа доступа "Свойство" является перемещение ВСЕХ аннотаций из свойств элемента в соответствующие геттеры. Большое предостережение состоит в том, чтобы не смешивать типы доступа "Поле" и "Свойство" в классе сущности, иначе поведение не определено спецификациями JSR-317.

Ответ 2

Решение простое, просто используйте CascadeType.MERGE вместо CascadeType.PERSIST или CascadeType.ALL.

У меня была та же проблема, и CascadeType.MERGE работал на меня.

Надеюсь, вы отсортированы.

Ответ 3

Использование слияния является рискованным и сложным, поэтому это грязное обходное решение в вашем случае. Вы должны помнить, что, когда вы передаете объект сущности для объединения, он перестает прикрепляться к транзакции, и вместо этого возвращается новый, теперь прикрепленный объект. Это означает, что если у кого-то есть объект старой сущности, который все еще находится в их распоряжении, изменения в нем молча игнорируются и отбрасываются при фиксации.

Здесь вы не видите полный код, поэтому я не могу дважды проверить ваш шаблон транзакции. Один из способов добраться до такой ситуации - если у вас нет транзакции, действующей при выполнении слияния и сохраняющейся. Ожидается, что в этом случае поставщик непрерывности откроет новую транзакцию для каждой операции JPA, которую вы выполняете, и немедленно зафиксируйте и закройте ее до возврата вызова. Если это так, слияние будет выполняться в первой транзакции, а затем после того, как метод слияния вернется, транзакция будет завершена и закрыта, а возвращенный объект теперь будет отсоединен. Сохраняющееся ниже оно откроет вторую транзакцию и попытается обратиться к объекту, который отсоединен, предоставив исключение. Всегда заверните свой код внутри транзакции, если вы не знаете, что вы делаете.

Используя транзакцию, управляемую контейнером, она будет выглядеть примерно так. Обратите внимание: это предполагает, что метод находится внутри сеансового компонента и вызывается через локальный или удаленный интерфейс.

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

Ответ 4

Удалить каскад из дочерней сущности Transaction, это должно быть просто:

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

(FetchType.EAGER можно удалить так же, как и по умолчанию для @ManyToOne)

Все это!

Зачем? Говоря "каскадировать ВСЕ" в Transaction дочерней сущности, вы требуете, чтобы каждая операция БД распространялась в Account родительской сущности. Если вы затем persist(transaction), будет также вызвано persist(account).

Но только временные (новые) сущности могут быть переданы для persist (Transaction в этом случае). Отсоединенные (или другие не переходные состояния) могут не (Account в этом случае, как это уже в БД).

Поэтому вы получаете исключение "отдельная сущность передана для сохранения". Под сущностью Account подразумевается! Не та Transaction вы звоните, persist.


Как правило, вы не хотите размножаться от ребенка к родителю. К сожалению, есть много примеров кода в книгах (даже в хороших) и в сети, которые делают именно это. Я не знаю, почему... Может быть, иногда просто копировать снова и снова, не задумываясь...

Угадайте, что произойдет, если вы вызовете метод remove(transaction) все еще есть "cascade ALL" в @ManyToOne? account (кстати, со всеми другими транзакциями!) Также будет удалена из БД. Но это не было твоим намерением, не так ли?

Ответ 5

Вероятно, в этом случае вы получили свой объект account, используя логику слияния, а persist используется для сохранения новых объектов, и он будет жаловаться, если иерархия имеет уже сохраненный объект. Вы должны использовать saveOrUpdate в таких случаях вместо persist.

Ответ 6

Не пропускайте id (pk) для сохранения метода или попробуйте метод save() вместо persist().

Ответ 7

В определении вашей сущности вы не указываете @JoinColumn для Account, присоединенного к Transaction. Вы хотите что-то вроде этого:

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

ОБНОВЛЕНИЕ: Ну, я думаю, это было бы полезно, если бы вы использовали аннотацию @Table в своем классе. Хех. :)

Ответ 8

Если ничего не помогает, и вы все еще получаете это исключение, просмотрите методы equals() и не включайте в него дочернюю коллекцию. Особенно, если у вас есть глубокая структура встроенных коллекций (например, A содержит Bs, B содержит Cs и т.д.).

В примере 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!
    }
  }

В приведенном выше примере удалите транзакции из equals() проверок. Это связано с тем, что спящий режим будет подразумевать, что вы не пытаетесь обновить старый объект, но вы передаете новый объект для сохранения, всякий раз, когда вы меняете элемент в дочерней коллекции.
Разумеется, эти решения не будут соответствовать всем приложениям, и вы должны тщательно разработать то, что хотите включить в методы equals и hashCode.

Ответ 9

Даже если ваши аннотации правильно объявлены для правильного управления отношениями "один ко многим", вы все равно можете столкнуться с этим точным исключением. При добавлении нового дочернего объекта Transaction к подключенной модели данных вам нужно будет управлять значением первичного ключа - если только вы не должны. Если вы укажете первичное значение для дочернего объекта, объявленного следующим образом, перед вызовом persist(T), вы столкнетесь с этим исключением.

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

В этом случае аннотации объявляют, что база данных будет управлять генерированием значений первичного ключа сущности при вставке. Предоставление одного из них (например, с помощью идентификатора) вызывает это исключение.

Альтернативно, но фактически то же самое, это объявление аннотации приводит к тому же исключению:

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

Итак, не устанавливайте значение id в коде приложения, когда оно уже управляется.

Ответ 10

Может быть, это ошибка OpenJPA, при откате reset поля @Version, но pcVersionInit сохраняет true. У меня есть абстракция, которая объявила поле @Version. Я могу обойти это с помощью reset поля pcVersionInit. Но это не очень хорошая идея. Я думаю, что это не сработает, если у вас есть объект с каскадом.

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

Ответ 11

Вам необходимо установить транзакцию для каждой учетной записи.

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

Или это будет достаточно (если необходимо), чтобы установить идентификаторы на null со стороны.

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

Ответ 12

cascadeType.MERGE,fetch= FetchType.LAZY

Ответ 13

В моем случае я совершал транзакцию, когда использовался постоянный метод. После изменения сохраняются, чтобы сохранить метод, это было решено.

Ответ 14

Если вышеуказанные решения не работают, просто один раз прокомментируйте методы getter и setter класса сущности и не устанавливайте значение id. (Первичный ключ) Тогда это будет работать.

Ответ 15

@OneToMany (mappedBy = "xxxx", cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE}) работал для меня.

Ответ 16

Для решения проблемы необходимо выполнить следующие действия:

1. Снятие каскадирования дочерних ассоциаций

Итак, вам нужно удалить @CascadeType.ALL из ассоциации @ManyToOne. Дочерние объекты не должны каскадироваться с родительскими ассоциациями. Только родительские объекты должны каскадироваться к дочерним объектам.

@ManyToOne(fetch= FetchType.LAZY)

Обратите внимание, что я установил для атрибута fetch значение FetchType.LAZY, поскольку энергичная загрузка очень плохо влияет на производительность.

2. Установите обе стороны ассоциации

Всякий раз, когда у вас есть двунаправленная связь, вам нужно синхронизировать обе стороны, используя методы addChild и removeChild в родительской сущности:

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

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

Подробнее о том, почему важно синхронизировать оба конца двунаправленной ассоциации, читайте в этой статье.