Spring данные JPA @PreRemove ConcurrentModificationException при удалении из родительского объекта

У меня есть случай, когда участник может регистркурсы.

В основном у меня есть следующая конфигурация объекта (геттеры и сеттеры опущены, а также другие бесполезные свойства):

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

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "course")
    private Set<Registration> registrations;

}

@Entity
@Table(name = "participant")
public class Participant {

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "participant")
    private Set<Registration> registrations;

}

@Entity
@Table(name = "registration")
public class Registration {

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "course_id")
    private Course course;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "participant_id")
    private Participant participant;

    @PreRemove
    private void removeRegistrationFromHolderEntities() {
        course.getRegistrations().remove(this);
        participant.getRegistrations().remove(this);
    }

}

Затем я могу из моей модели просмотра удалить регистрацию или курс (я также удалил ненужные вещи):

@Command
public void deleteRegistration(Registration reg) {
    registrationMgr.delete(reg);
}

@Command
public void deleteCourse(Course crs) {
    courseMgr.delete(crs);
}

Проблема :

  • Если я удаляю регистрацию, мне нужна функция @PreRemove, чтобы я мог удалить ссылки. Без этого удаление игнорируется (без ошибки, просто игнорируется)
  • Если я удалю курс, мне придется удалить функцию @PreRemove, иначе я получу ConcurrentModificationException (очевидно...)

Я также не могу удалить ссылки из метода deleteRegistration (вместо @PreRemove), потому что регистрации участников загружаются лениво (вызовет исключение failed to lazily initialize a collection of role: ..., could not initialize proxy - no Session).

Каков наилучший подход здесь?

Я использую Ява 11 с Весенняя загрузка 1.0.4весна-загрузка-стартер-данные-jpa).

РЕДАКТИРОВАТЬ :

Менеджеры/репозитории или определены таким образом (то же самое для registration и participant), поэтому он должен быть транзакционным (у меня нет @EnableTransactionManagement в моем основном классе, но это не требуется, поскольку я не использую транзакции вне репозиториев):

@Transactional
@Component("courseMgr")
public class CourseManager {

    @Autowired
    CourseRepository courseRepository;

    public void saveOrUpdate(Course course) {
        courseRepository.save(course);
    }

    public void delete(Course course) {
        courseRepository.delete(course);
    }
}

public interface CourseRepository extends CrudRepository<Course, Long> {
    ...
}

РЕДАКТИРОВАТЬ2 :

Я думаю, что нашел довольно простое решение:

Я удалил метод @PreRemove из сущности, а затем вместо удаления подобных ссылок в методе deleteRegistration (который я пробовал, но вызывал исключение failed to lazily initialize a collection of role):

@Command
public void deleteRegistration(Registration reg) {
    reg.getCourse().getRegistrations().remove(reg);
    reg.getParticipant.getRegistrations().remove(reg);
    registrationMgr.delete(reg);
}

Я просто установил для родителей значение null, мне все равно, как оно будет удалено...

@Command
public void deleteRegistration(Registration reg) {
    reg.setCourse(null);
    reg.setParticipant(null);
    registrationMgr.delete(reg);
}

Так что теперь я также могу удалить курс, не вызывая ConcurrentModificationException в @PreRemove.

РЕДАКТИРОВАТЬ3 : Плохо, регистрация не была удалена с помощью приведенного выше решения (по-прежнему нет ошибки, но ничего не происходит). Вместо этого я закончил с этим, что, наконец, работает:

@Command
public void deleteRegistration(Registration reg) {
    // remove reference from course, else delete does nothing
    Course c = getRegistration().getCourse();
    c.getRegistrations().remove(getRegistration());
    courseMgr.saveOrUpdate(c);

    // delete registration from the database
    registrationMgr.delete(reg);
}

Не нужно удалять ссылку с участника...

Зачем нужен @PreRemove для удаления ссылок? Разве вы не должны управлять графом сущностей внутри транзакции?

K.Nicholas 26.04.2019 18:47

@K.Nicholas Мои менеджеры являются транзакционные (я отредактировал вопрос, добавив определение). У меня нет большого опыта работы с JPA, но я использую его не в первый раз, и у меня никогда не было таких проблем в других моих проектах, поэтому здесь должно быть что-то особенное.

Yann39 29.04.2019 10:12

Моя точка зрения/вопрос заключается в том, что кажется, что вы просто хотите удалить дочерние объекты Entity. Я действительно не читал все это слишком внимательно, но если это так, то я думаю, что @PreRemove не предназначен для этой цели, и поэтому меня не удивляет, что вы получаете ошибки. Если вы хотите удалить дочерние элементы или отношения сущности, вы должны делать все это с отдельными удалениями из отдельных репозиториев.

K.Nicholas 29.04.2019 21:26

Спасибо, я согласен, что @PreRemove может быть не предназначен для этой цели, но это единственный способ, который я нашел для удаления ссылок в коллекциях. Выполнение этого в методе deleteRegistration всегда вызывает исключение failed to lazily initialize a collection of roles или ничего не делает. Я пробовал использовать @EnableTransactionManagement + @Transactional в методе, вызывать .size() перед удалением или даже использовать spring.jpa.properties.hibernate.enable_lazy_load_no_trans=tr‌​ue, ничего не работает, либо я получаю сообщение об ошибке, либо ничего не происходит.

Yann39 30.04.2019 10:17

Вы говорите, что когда вы удаляете регистрацию, она игнорируется без ошибок, но я так не думаю. Я думаю, что его удаляют, а вы все еще видите его в регистрациях и думаете, что он не был удален. Либо так, либо он удаляется из базы данных, а каскад добавляет его обратно. Не знаю, не проверял. Смотрите мои комментарии ниже.

K.Nicholas 01.05.2019 07:38

Я включил трассировку JPA и увидел, что запрос DELETE не выдавался, запись в БД осталась, а ID не изменился. Как будто полностью игнорируется. В зависимости от того, что я прочитал, может быть несколько причин (например, это или это или это, ...), но на самом деле ничего не относится к моему случаю. Во всяком случае, я нашел довольно простое решение, см. edit2 в вопросе. Большое спасибо за уделенное время!

Yann39 01.05.2019 14:00
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
6
1 939
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы неправильно настроили свои репозитории. Вам нужен составной PK для Registration, и вы должны понимать, что двунаправленные сопоставления действительно предназначены только для запросов. Кроме того, двунаправленные сопоставления в Course и Participate представляют собой проблемы, потому что связь ManyToOne через сущность Registration по умолчанию является FetchType.EAGER. Со всеми имеющимися у вас аннотациями cascade и fetch вы запрашиваете сложную комбинацию вещей из JPA, и кажется, что вы еще не во всем разобрались. Начните с основ, обязательно распечатайте свои операторы SQL и продолжайте оттуда, если хотите попробовать больше утонченности JPA.

@Entity
@Data
public class Course {
    @Id
    private Integer id;
    private String name;
}

@Entity
@Data
public class Participant {
    @Id
    private Integer id;
    private String name;
}

@Entity
@Data
public class Registration {
    @EmbeddedId
    private RegistrationPK id;

    @ManyToOne
    @MapsId("participant_id")
    private Participant participant;

    @ManyToOne
    @MapsId("course_id")
    private Course course;
}

@Embeddable
@Data
public class RegistrationPK implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer course_id;
    private Integer participant_id;
}

Является ли ваш основной Entities. RegistrationRepository нужен дополнительный запрос.

public interface RegistrationRepository extends JpaRepository<Registration, RegistrationPK> {
    Set<Registration> findByCourse(Course c);
}

И использовать все это в примере:

@Override
public void run(String... args) throws Exception {
    create();
    Course c = courseRepo.getOne(1);
    Set<Registration> rs = read(c);
    System.out.println(rs);
    deleteCourse(c);
}

private void create() {
    Course c1 = new Course();
    c1.setId(1);
    c1.setName("c1");
    courseRepo.save(c1);

    Participant p1 = new Participant();
    p1.setId(1);
    p1.setName("p1");
    participantRepo.save(p1);

    Registration r1 = new Registration();
    r1.setId(new RegistrationPK());
    r1.setCourse(c1);
    r1.setParticipant(p1);
    registrationRepo.save(r1);
}

private Set<Registration> read(Course c) {
    return registrationRepo.findByCourse(c);
}

private void deleteCourse(Course c) {
    registrationRepo.deleteAll( registrationRepo.findByCourse(c) );
    courseRepo.delete(c);
}

Это может быть решением, но я хочу избежать изменения основной структуры моей базы данных/приложения, которая в настоящее время работает в производстве (меня только что попросили добавить функцию удаления курса). А с двунаправленными маппами очень удобно работать в коде (т.е. мне очень часто приходится отображать регистрации курсов), поэтому я хочу их сохранить. В любом случае, спасибо за этот другой подход, мне все еще нужно узнать обо всей магии, которую JPA делает под сценой :)

Yann39 01.05.2019 14:10
Ответ принят как подходящий

ОК, решение было довольно простым.

Мне действительно нужно удалить ссылки из метода deleteRegistration. Это то, что я пробовал, но вызывал исключение failed to lazily initialize a collection of role:

@Command
public void deleteRegistration(Registration reg) {
    reg.getCourse().getRegistrations().remove(reg);
    reg.getParticipant.getRegistrations().remove(reg);
    registrationMgr.delete(reg);
}

Хитрость в том, что я также должен сохранить объект курса, прежде чем пытаться удалить регистрацию.

Это работает :

@Command
public void deleteRegistration(Registration reg) {
    // remove reference from course, else delete does nothing
    Course c = getRegistration().getCourse();
    c.getRegistrations().remove(getRegistration());
    courseMgr.saveOrUpdate(c);

    // delete registration from the database
    registrationMgr.delete(reg);
}

Не нужно удалять ссылку с участника...

@PreRemove выполнял свою работу, но теперь я также могу удалить курс, не вызывая ConcurrentModificationException.

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