PersistentObjectException: отсоединенная сущность, переданная для сохранения, выброшенная JPA и Hibernate

237

У меня есть сохраненная в 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объектом?

Пол Санвальд
источник
15
В моем случае я устанавливал id объекта, который пытался сохранить с помощью Entity Manager. Когда я удалил сеттер для id, он начал работать нормально.
Руши Шах,
В моем случае я не устанавливал идентификатор, но было два пользователя, использующих одну и ту же учетную запись, один из них сохранил сущность (правильно), и ошибка произошла, когда второй последний попытался сохранить ту же сущность, которая уже была сохранялось.
sergioFC

Ответы:

129

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

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

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

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

Sym-Sym
источник
2
ps: аннотация @Id - это та, которую hibernate использует для определения типа доступа.
Диего Пленц
2
Исключение: detached entity passed to persistпочему улучшение согласованности делает его работоспособным? Хорошо, согласованность была исправлена, но объект все еще отсоединен.
Гильгамес
1
@ Сэм, большое спасибо за объяснение. Но я до сих пор не понимаю. Я вижу, что без «специального» сеттера двунаправленные отношения не удовлетворяются. Но я не понимаю, почему объект был отделен.
Гильгамес
28
Пожалуйста, не публикуйте ответ, который отвечает исключительно ссылками.
Эль Мак,
6
Не можете увидеть, как этот ответ связан с вопросом вообще?
Евгений Лабун
271

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

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

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

ochiWlad
источник
5
Удивительно, но у меня это тоже сработало. Это не имеет смысла, поскольку CascadeType.ALL включает в себя все другие типы каскадов ... WTF? У меня весна 4.0.4, весна данных jpa 1.8.0 и hibernate 4.X .. У кого-нибудь есть мысли, почему ВСЕ не работает, а MERGE работает?
Вадим Кирилчук
22
@VadimKirilchuk Это сработало и для меня, и это имеет смысл. Поскольку транзакция PERSISTED, она также пытается PERSIST Account, и это не работает, так как Account уже находится в базе данных. Но с CascadeType.MERGE учетная запись автоматически объединяется.
Стрелок
4
Это может произойти, если вы не используете транзакции.
Lanoxx
1
другое решение: постарайтесь не вставлять уже сохраненный объект :)
Андрей Плотников
3
Спасибо, мужик. Невозможно избежать вставки сохраняемого объекта, если у вас есть ограничение, чтобы ссылочный ключ был NOT NULL. Так что это единственное решение. Еще раз спасибо
Маккаси
22

Удалить каскад из дочерней сущности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)прежнему называете «каскадом ВСЕ» в этом @ManyToOne? account(Кстати, со всеми другими транзакциями!) Будут удалены из базы данных , а также. Но это не было твоим намерением, не так ли?

Евгений Лабун
источник
Просто хочу добавить, что если вы действительно хотите сохранить потомка вместе с родителем, а также удалить родителя вместе с потомком, например, person (parent) и address (child) вместе с addressId, автоматически сгенерированным DB, то перед вызовом save on Person просто сделайте вызов, чтобы сэкономить на адресе в вашем методе транзакции. Таким образом, он будет сохранен вместе с идентификатором, также сгенерированным БД. Не влияет на производительность, так как Hibenate по-прежнему выполняет 2 запроса, мы просто меняем порядок запросов.
Викки
Если мы не можем ничего передать, то какое значение по умолчанию оно принимает для всех случаев.
Дхванил Патель
15

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

Вы не видите полный код здесь, поэтому я не могу дважды проверить ваш шаблон транзакции. Один из способов попасть в такую ​​ситуацию - это если у вас нет активной транзакции при выполнении слияния и сохраниться. В этом случае ожидается, что поставщик сохраняемости будет открывать новую транзакцию для каждой выполняемой вами операции 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);
}
ЦУС
источник
Я сталкиваюсь с той же проблемой. Я использовал @Transaction(readonly=false)на уровне обслуживания . Я все еще получаю ту же проблему,
Сентил Арумугам SP
Я не могу сказать, что полностью понимаю, почему все работает именно так, но размещение метода и представления персистентности в сопоставлении сущностей вместе в аннотации транзакции устранило мою проблему, так что спасибо.
Deadron
Это на самом деле лучшее решение, чем наиболее одобренное.
Алексей Майде
13

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

Дан
источник
3
это JPA, поэтому я думаю, что аналогичным методом является .merge (), но это дает мне то же исключение. Для ясности, Транзакция - это новый объект, а Аккаунт - нет.
Пол Санвальд
@PaulSanwald Использование mergeна transactionобъекте вы получаете ту же ошибку?
Дан
на самом деле, нет, я неправильно сказал. если я .merge (транзакция), то транзакция не сохраняется вообще.
Пол Санвальд
@PaulSanwald Хм, вы уверены, что transactionэто не сохранилось? Как ты проверил Обратите внимание, что mergeвозвращается ссылка на постоянный объект.
дан
объект, возвращаемый .merge (), имеет нулевой идентификатор. Кроме того, я делаю .findAll () после этого, и мой объект не там.
Пол Санвальд
12

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

Ангад Бансоде
источник
2
Хороший совет! Но только если идентификатор генерируется. В случае, если он назначен, это нормально, чтобы установить идентификатор.
Алдиан,
это сработало для меня. Кроме того, вы можете использовать его TestEntityManager.persistAndFlush(), чтобы сделать экземпляр управляемым и постоянным, а затем синхронизировать контекст постоянства с базовой базой данных. Возвращает исходный объект
Анюль Ривас,
7

Поскольку это очень распространенный вопрос, я написал эту статью , на которой основан этот ответ.

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

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

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

Влад Михалча
источник
4

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

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

РЕДАКТИРОВАТЬ: Ну, я думаю, это было бы полезно, если бы вы использовали @Tableаннотацию на вашем классе. Хех. :)

NemesisX00
источник
2
да, я не думаю, что это все-таки я добавил @JoinColumn (name = "fromAccount_id", referencedColumnName = "id") и это не сработало :).
Пол Санвальд
Да, я обычно не использую файл сопоставления xml для сопоставления сущностей с таблицами, поэтому я обычно предполагаю, что он основан на аннотациях. Но если бы мне пришлось угадывать, вы используете hibernate.xml для сопоставления сущностей с таблицами, верно?
NemesisX00
нет, я использую пружинные данные JPA, поэтому все основано на аннотациях. У меня есть аннотация "mappedBy" на другой стороне: @OneToMany (cascade = {CascadeType.ALL}, fetch = FetchType.EAGER, mappedBy = "fromAccount")
Пол Санвальд,
4

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

Дан
источник
4

Если ничего не помогает, и вы все еще получаете это исключение, проверьте свои equals()методы - и не включайте в него дочернюю коллекцию. Особенно, если у вас есть глубокая структура встроенных коллекций (например, A содержит B, 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методах.

FazoM
источник
1

Возможно, это ошибка 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;
            }
Yaocl
источник
1

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

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.
stakahop
источник
1
cascadeType.MERGE,fetch= FetchType.LAZY
Джеймс Шива
источник
1
Привет Джеймс и добро пожаловать, вы должны стараться избегать только ответов кода. Пожалуйста, укажите, как это решает проблему, указанную в вопросе (и когда это применимо или нет, уровень API и т. Д.).
Maarten Bodewes
Рецензенты VLQ : см. Meta.stackoverflow.com/questions/260411/… . Ответы только для кода не заслуживают удаления, соответствующее действие - выбрать «Выглядит нормально».
Натан Хьюз
1

Мой ответ на основе Spring Data JPA: я просто добавил @Transactionalаннотацию к внешнему методу.

Почему это работает

Дочерняя сущность немедленно становилась отсоединенной, поскольку не было активного контекста Hibernate Session. Предоставление транзакции Spring (Data JPA) гарантирует наличие сеанса Hibernate.

Ссылка:

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

Джей Джей Забкар
источник
0

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

H.Ostwal
источник
0

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

Shubham
источник
0

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

SateeshKasaboina
источник
0

Разрешается сохранение зависимого объекта до следующего.

Это случилось со мной, потому что я не устанавливал Id (который не был сгенерирован автоматически). и пытается сохранить с отношением @ManytoOne

Шахид Хуссейн Аббаси
источник