PersistentObjectException: відокремлений об'єкт передається для збереження, кинутого JPA та Hibernate


237

У мене є об'єктна модель, що зберігається в JPA, яка містить відносини "багато в одному": " Accountє багато" Transactions. A 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 на 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
У моєму випадку я встановлював ідентифікатор об'єкта, який я намагався зберегти, використовуючи Entity Manager. Коли я видалив сетер для id, він почав працювати нормально.
Руші Шах

У моєму випадку я не встановлював ідентифікатор, але було два користувачі, що використовували один і той же обліковий запис, один з них зберігав сутність (правильно), і помилка виникла, коли другий останній намагався зберегти ту саму сутність, це вже було зберігався.
sergioFC

Відповіді:


129

Це типова проблема двосторонньої узгодженості. Це добре обговорюється в цьому посиланні , а також у цьому посиланні.

Відповідно до статей попередніх 2 посилань, вам потрібно виправити налаштування в обох сторонах двонаправлених відносин. Приклад встановлення для однієї сторони знаходиться в цьому посиланні.

Приклад сеттера для сторони «Багато» знаходиться за цим посиланням.

Після виправлення налаштувань ви хочете оголосити тип доступу для Entity як "Властивість". Найкраща практика оголошення типу "Властивості" - переміщення ВСІХ приміток із властивостей учасника до відповідних одержувачів. Велике застереження - не змішувати типи доступу «Поле» та «Властивість» у класі сутності, інакше поведінка не визначена специфікаціями JSR-317.


2
ps: анотація @Id - це те, що використовується в сплячому режимі для ідентифікації типу доступу.
Дієго Пленц

2
Виняток: detached entity passed to persistЧому поліпшення консистенції примушує його працювати? Гаразд, консистенція була відремонтована, але об'єкт все ще відокремлений.
Гільгамеш

1
@Sam, дякую за ваше пояснення. Але я ще не розумію. Я бачу, що без "спеціального" сетера двоспрямовані відносини не задовольняються. Але я не бачу, чому об’єкт був відокремлений.
Гільгамеш

28
Не публікуйте відповідь, яка відповідає виключно за посиланнями.
El Mac

6
Ви не бачите, як ця відповідь взагалі пов'язана з питанням?
Євген Лабун

270

Рішення просте, просто використовуйте CascadeType.MERGEзамість CascadeType.PERSISTабо CascadeType.ALL.

У мене була така ж проблема і CascadeType.MERGEпрацює на мене.

Я сподіваюся, що ви відсортовані.


5
Дивно, але одна працювала і на мене. Немає сенсу, оскільки CascadeType.ALL включає всі інші типи каскаду ... WTF? У мене весна 4.0.4, весняні дані jpa 1.8.0 і сплячий режим 4.X. Хтось має думки, чому ВСІ не працюють, але МЕРЕЖ?
Вадим Кирильчук

22
@VadimKirilchuk Це теж працювало для мене, і це має повний сенс. Оскільки транзакція ПЕРСИСТИЧНА, вона також намагається виконати ПЕРСИСТИЧНИЙ рахунок, і це не працює, оскільки Обліковий запис вже є у db. Але з CascadeType.MERGE замість цього автоматично обліковий запис об'єднується.
Gunslinger

4
Це може статися, якщо ви не використовуєте транзакції.
lanoxx

1
інше рішення: постарайтеся не вставляти вже збережений об’єкт :)
Андрій Плотніков

3
Спасибі, чоловіче. Не можна уникнути вставки збереженого об'єкта, якщо у вас обмеження для опорного ключа НЕ НУЛЬНЕ. Тож це єдине рішення. Ще раз дякую вам.
makkasi

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(До речі, з усіма іншими транзакціями!) Будуть видалені з бази даних , а також. Але це був не ваш намір, чи не так?


Просто хочу додати, якщо ваш намір дійсно зберегти дитину разом з батьком, а також видалити батьків разом із дитиною, як, наприклад, особа (батько) та адреса (дитина) разом із адресою Id, автоматично згенерованою БД, то перед тим, як викликати збереження на Особі, просто зробіть дзвінок, щоб зберегти адресу в способі транзакції. Таким чином, це було б збережено разом з id, що також генерується БД. Не впливаючи на продуктивність, оскільки Hibenate все ще робить 2 запити, ми просто змінюємо порядок запитів.
Vikky

Якщо ми нічого не можемо передати, то яке значення за замовчуванням воно буде у всіх випадках.
Dhwanil Patel

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)сервісний рівень. Я все ще отримую ту саму проблему,
Senthil Arumugam SP

Я не можу сказати, що я повністю розумію, чому все працює таким чином, але розміщення методу збереження та перегляд об'єднання відображення разом у примітці про трансакцію вирішило мою проблему, тому дякую.
Дедрон

Це насправді краще рішення, ніж найвигідніше.
Олексій Мейде

13

Ймовірно, у цьому випадку ви отримали свій accountоб'єкт, використовуючи логіку злиття, і persistвін використовується для збереження нових об'єктів, і він буде скаржитися, якщо в ієрархії вже є об'єкт, що зберігається. Ви повинні використовувати saveOrUpdateв таких випадках, а не persist.


3
це JPA, тому я думаю, що аналогічним методом є .merge (), але це дає мені той самий виняток. Щоб було зрозуміло, транзакція - це новий об’єкт, рахунок - ні.
Пол Санвальд

@PaulSanwald Використовуючи mergeна transactionоб’єкті, ви отримуєте ту ж помилку?
дан

насправді, ні, я неправильно говорив. якщо я .merge (транзакція), транзакція взагалі не зберігається.
Пол Санвальд

@PaulSanwald Хм, ти впевнений, що transactionне зберігався? Як ви перевірили. Зауважте, що mergeповертається посилання на збережений об'єкт.
дан

об’єкт, повернутий .merge (), має нульовий ідентифікатор. Крім того, я роблю .findAll () згодом, і мій об’єкт там немає.
Пол Санвальд

12

Не передайте id (pk) для збереження методу чи спробуйте зберегти () метод замість 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;
}

EDIT: Ну, мабуть, це було б корисно, якби ви використовували @Tableанотацію на своєму уроці. Хе. :)


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

У цьому випадку в анотаціях заявляється, що після вставки база даних керуватиме генерацією значень первинного ключа об'єкта. Забезпечення себе (наприклад, через сетер 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 містить 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методи.


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

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.

1
cascadeType.MERGE,fetch= FetchType.LAZY

1
Привіт, Джеймс і ласкаво просимо, ви повинні намагатися уникати відповідей лише у коді. Будь ласка, вкажіть, як це вирішує проблему, вказану у запитанні (і коли це застосовується чи ні, рівень API тощо).
Maarten Bodewes

Рецензенти VLQ: див. Meta.stackoverflow.com/questions/260411/… . відповіді, що стосуються лише коду, не заслуговують на видалення, відповідною дією є вибір "Виглядає добре".
Натан Х'юз

1

Моя відповідь на основі весняних даних JPA: Я просто додав @Transactionalпримітку до свого зовнішнього методу.

Чому це працює

Дочірнє утворення одразу стає відокремленим, оскільки не було активного контексту сплячого сеансу. Забезпечення весняної транзакції (Data JPA) гарантує наявність сплячої сесії.

Довідка:

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


0

У моєму випадку я угода здійснення , коли НЕ проходить використовував метод. Після зміни методу збереження для збереження він вирішився.


0

Якщо вищезгадані рішення не спрацьовують лише один раз, коментуйте методи getter та setter класу сутності та не встановлюйте значення id. (Первинний ключ), тоді це спрацює.



0

Вирішується збереженням залежного об'єкта до наступного.

Це сталося зі мною, оскільки я не встановлював Id (який не генерувався автоматично). і намагається зберегти відношення @ManytoOne

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.