it-roy-ru.com

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

У меня есть сохраненная в JPA объектная модель, которая содержит отношение многие-к-одному: на счету много транзакций. Транзакция имеет одну учетную запись.

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

@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. Но когда я создаю транзакцию, используя существующую уже сохраненную учетную запись и сохраняя транзакцию , я получаю исключение:

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

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

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

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

164
Paul Sanwald

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

Согласно статьям в предыдущих двух ссылках, вам необходимо установить сеттеры по обе стороны двунаправленных отношений. Пример сеттера для One side находится в по этой ссылке.

Пример сеттера для стороны Many находится в эта ссылка.

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

104
Sym-Sym

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

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

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

198
ochiWlad

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

Вы не видите полный код здесь, поэтому я не могу дважды проверить ваш шаблон транзакции. Один из способов попасть в подобную ситуацию - это если у вас нет активной транзакции при выполнении слияния и сохраниться. В этом случае ожидается, что поставщик сохраняемости будет открывать новую транзакцию для каждой выполняемой вами операции 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);
}
10
Zds

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

9
dan

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

7
Angad Bansode

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

В примере 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(). Это связано с тем, что hibernate будет означать, что вы не пытаетесь обновить старый объект, а передаете новый объект для сохранения при каждом изменении элемента в дочерней коллекции.
Конечно, это решение не подходит для всех приложений, и вам следует тщательно продумать, что вы хотите включить в методы equals и hashCode.

4
FazoM

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

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

Правка: Ну, я думаю, это было бы полезно, если бы вы использовали аннотацию @Table в вашем классе. Хех. :)

4
NemesisX00

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

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

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

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

Прежде всего, спасибо за очень хорошее описание и пример, демонстрирующий проблему.

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

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

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

Это все!

Зачем? Говоря «каскадировать ВСЕ» на дочернем объекте, вы требуете, чтобы каждая операция БД распространялась на родительский объект. Если вы затем выполните persist(transaction), также будет вызвана persist(account). Но только постоянные (новые) сущности могут быть переданы в persist (), а отсоединенные (в данном случае transaction уже находится в БД) - нет. Поэтому вы получаете исключение "отсоединенная сущность, переданная для сохранения"

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

1
Eugen Labun

Возможно, это ошибка OpenJPA, при откате она сбрасывает поле @Version, но pcVersionInit сохраняет значение true. У меня есть AbstraceEntity, который объявил поле @Version. Я могу обойти это путем сброса поля 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;
            }
1
Yaocl

Даже если ваши аннотации объявлены правильно для правильного управления отношениями «один ко многим», вы все равно можете столкнуться с этим точным исключением. При добавлении нового дочернего объекта 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 в коде приложения, когда оно уже управляется.

1
dan
cascadeType.MERGE,fetch= FetchType.LAZY
0
James Siva

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

0
H.Ostwal