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);
}
Подробнее о том, почему важно синхронизировать оба конца двунаправленной ассоциации, читайте в этой статье.