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

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

24 ответа

Решение

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

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

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

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

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

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

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

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

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

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

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

Это все!

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

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

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

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

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

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

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

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

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

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

Старый вопрос, но недавно столкнулся с той же проблемой. Делюсь своим опытом здесь.

Юридическое лицо

      @Data
@Entity
@Table(name = "COURSE")
public class Course  {

    @Id
    @GeneratedValue
    private Long id;
}

Сохранение сущности (JUnit)

      Course course = new Course(10L, "testcourse", "DummyCourse");
testEntityManager.persist(course);

Исправить

      Course course = new Course(null, "testcourse", "DummyCourse");
testEntityManager.persist(course);

Заключение: если класс сущности имеет @GeneratedValue для первичного ключа (id), убедитесь, что вы не передаете значение для первичного ключа (id).

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

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

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

Ссылка:

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

Если ничего не помогает, и вы все еще получаете это исключение, просмотрите 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 методы.

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

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

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

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

Вот мое исправление.

Ниже моя сущность. Отметьте, что идентификатор аннотируется @GeneratedValue(strategy = GenerationType.AUTO), что означает, что идентификатор будет сгенерирован Hibernate. Не устанавливайте его при создании объекта сущности . Поскольку это будет автоматически сгенерировано файлом Hibernate.Имейте в виду, что если поле идентификатора объекта не помечено @GeneratedValue, то не присвоение идентификатора значения вручную также является преступлением, которое будет встречено с помощью IdentifierGenerationException: идентификаторы для этого класса должны быть назначены вручную перед вызовом save()

      @Entity
@Data
@NamedQuery(name = "SimpleObject.findAll", query="Select s FROM SimpleObject s")
public class SimpleObject {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String key;

    @Column
    private String value;

}

А вот и мой основной класс.

      public class SimpleObjectMain {

    public static void main(String[] args) {

        System.out.println("Hello Hello From SimpleObjectMain");

        SimpleObject simpleObject = new SimpleObject();
        simpleObject.setId(420L); // Not right, when id is a generated value then no need to set this.
        simpleObject.setKey("Friend");
        simpleObject.setValue("Bani");

        EntityManager entityManager = EntityManagerUtil.getEntityManager();
        entityManager.getTransaction().begin();
        entityManager.persist(simpleObject);
        entityManager.getTransaction().commit();

        List<SimpleObject> simpleObjectList = entityManager.createNamedQuery("SimpleObject.findAll").getResultList();
        for(SimpleObject simple : simpleObjectList){
            System.out.println(simple);
        }

        entityManager.close();
        
    }
}

Когда я пытался спасти это, он бросал это

      PersistentObjectException: detached entity passed to persist.

Все, что мне нужно было исправить, - это удалить эту строку настройки идентификатора для simpleObject в основном методе.

Может быть, это ошибка 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;
            }
cascadeType.MERGE,fetch= FetchType.LAZY

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

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

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

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

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.

Другая причина, по которой я столкнулся с этой проблемой, - наличие сущностей, для которых не используется версия Hibernate в транзакции.

Добавить @Version аннотация ко всем отображаемым объектам

      @Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;

    @Version
    private Integer version;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "orders")
    private CustomerOrders orders;

}
      @Entity
public class CustomerOrders {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;

    @Version
    private Integer version;

    private BigDecimal value;

}

Проблема здесь в отсутствии контроля.

Когда мы используем метод сохранения CrudRepository/JPARepository, мы теряем контроль над транзакциями.

Чтобы решить эту проблему, у нас есть Управление транзакциями.

я предпочитаю@Transactionalмеханизм

импорт

      import javax.transaction.Transactional;

Весь исходный код:

      package com.oracle.dto;

import lombok.*;

import javax.persistence.*;
import java.util.Date;
import java.util.List;

@Entity
@Data
@ToString(exclude = {"employee"})
@EqualsAndHashCode(exclude = {"employee"})
public class Project {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO,generator = "ps")
    @SequenceGenerator(name = "ps",sequenceName = "project_seq",initialValue = 1000,allocationSize = 1)
    @Setter(AccessLevel.NONE)
    @Column(name = "project_id",updatable = false,nullable = false)
    private Integer pId;
    @Column(name="project_name",nullable = false,updatable = true)
    private String projectName;
    @Column(name="team_size",nullable = true,updatable = true)
    private Integer teamSize;
    @Column(name="start_date")
    private Date startDate;
    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name="projectemp_join_table",
        joinColumns = {@JoinColumn(name = "project_id")},
        inverseJoinColumns = {@JoinColumn(name="emp_id")}
    )
    private List<Employee> employees;
}
      package com.oracle.dto;

import lombok.*;

import javax.persistence.*;
import java.util.List;

@Entity
@Data
@EqualsAndHashCode(exclude = {"projects"})
@ToString(exclude = {"projects"})
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO,generator = "es")
    @SequenceGenerator(name = "es",sequenceName = "emp_seq",allocationSize = 1,initialValue = 2000)
    @Setter(AccessLevel.NONE)
    @Column(name = "emp_id",nullable = false,updatable = false)
    private Integer eId;
    @Column(name="fist_name")
    private String firstName;
    @Column(name="last_name")
    private String lastName;
    @ManyToMany(mappedBy = "employees")
    private List<Project> projects;
}


      package com.oracle.repo;

import com.oracle.dto.Employee;
import org.springframework.data.jpa.repository.JpaRepository;

public interface EmployeeRepo extends JpaRepository<Employee,Integer> {
}

      package com.oracle.repo;

import com.oracle.dto.Project;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProjectRepo extends JpaRepository<Project,Integer> {
}

      package com.oracle.services;

import com.oracle.dto.Employee;
import com.oracle.dto.Project;
import com.oracle.repo.ProjectRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.transaction.Transactional;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

@Component
public class DBServices {
    @Autowired
    private ProjectRepo repo;
    @Transactional
    public void performActivity(){

        Project p1 = new Project();
        p1.setProjectName("Bank 2");
        p1.setTeamSize(20);
        p1.setStartDate(new Date(2020, 12, 22));

        Project p2 = new Project();
        p2.setProjectName("Bank 1");
        p2.setTeamSize(21);
        p2.setStartDate(new Date(2020, 12, 22));

        Project p3 = new Project();
        p3.setProjectName("Customs");
        p3.setTeamSize(11);
        p3.setStartDate(new Date(2010, 11, 20));

        Employee e1 = new Employee();
        e1.setFirstName("Pratik");
        e1.setLastName("Gaurav");

        Employee e2 = new Employee();
        e2.setFirstName("Ankita");
        e2.setLastName("Noopur");

        Employee e3 = new Employee();
        e3.setFirstName("Rudra");
        e3.setLastName("Narayan");

        List<Employee> empList1 = new LinkedList<Employee>();
        empList1.add(e2);
        empList1.add(e3);

        List<Employee> empList2 = new LinkedList<Employee>();
        empList2.add(e1);
        empList2.add(e2);

        List<Project> pl1=new LinkedList<Project>();
        pl1.add(p1);
        pl1.add(p2);

        List<Project> pl2=new LinkedList<Project>();
        pl2.add(p2);pl2.add(p3);

        p1.setEmployees(empList1);
        p2.setEmployees(empList2);

        e1.setProjects(pl1);
        e2.setProjects(pl2);

        repo.save(p1);
        repo.save(p2);
        repo.save(p3);

    }
}

Итак, я наткнулся на этот вопрос и ответы, потому что получил ту же ошибку, но очень простой объект только со строками и целыми числами.

Но в моем случае я пытался установить значение для поля, которое было аннотировано .

Итак, если вы используете@Idкажется, что вы не можете создать новый объект в классе и самостоятельно установить идентификатор и сохранить его в базе данных. Затем вы должны оставить поле Id пустым. Я не знал, и, возможно, это поможет кому-то еще.

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

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

Эта ошибка исходит из жизненного цикла JPA. Чтобы решить, не нужно использовать специальный декоратор. Просто присоединитесь к объекту, используя слияние следующим образом:

      entityManager.merge(transaction);

И не забудьте правильно настроить геттер и сеттер, чтобы обе стороны были синхронизированы.

Другие вопросы по тегам