【问题标题】:Spring data JPA @PreRemove ConcurrentModificationException when removing from parent enity从父实体中删除时的Spring数据JPA @PreRemove ConcurrentModificationException
【发布时间】:2019-04-26 14:24:38
【问题描述】:

我有一个案例,参与者可以注册 课程

基本上我有以下实体配置(省略了getter和setter以及其他无用的属性):

@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 异常)。

这里最好的方法是什么?

我使用 Java 11Spring Boot 1.0.4(以及 spring-boot-starter-data-jpa)。

编辑

经理/存储库或以这种方式定义(registrationparticipant 相同)所以它应该是事务性的(我的主类上没有 @EnableTransactionManagement,但它不应该是必需的,因为我没有' t 使用存储库之外的事务):

@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> {
    ...
}

EDIT2

我想我找到了一个非常简单的解决方案:

我已经从实体中删除了 @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);
}

我只是将父母设置为空,我不在乎,因为它会被删除...

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

所以现在我也可以在不触发@PreRemove 中的ConcurrentModificationException 的情况下删除课程。

EDIT3:我的错,上面的解决方案没有删除注册(仍然没有错误,但没有任何反应)。我以这个结束了,它终于奏效了:

@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 我的经理事务性的(我编辑了问题以添加定义)。我对 JPA 没有太多经验,但这不是我第一次使用它,而且我在其他项目中从未遇到过此类问题,所以这里应该有一些特别的东西。
  • 我的观点/问题是,您似乎只想删除实体的子级。我真的没有仔细阅读所有这些内容,但如果是这样的话,那么我认为@PreRemove 不是为此目的,所以你遇到错误并不让我感到惊讶。如果你想删除一个实体的子实体或关系,你应该从单个 repos 中删除所有这些。
  • 谢谢,我同意@PreRemove 可能不用于此目的,但这是我发现删除集合中的引用的唯一方法。在deleteRegistration 方法中执行此操作总是会引发failed to lazily initialize a collection of roles 异常或什么也不做。我试过在方法上使用@EnableTransactionManagement + @Transactional,在删除之前调用.size(),甚至使用spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true,都没有工作,要么我收到错误,要么什么也没发生。
  • 你说当你删除一个注册时,它会被忽略,没有错误,但我不这么认为。我认为它正在被删除,您仍然在注册中看到它并认为它没有被删除。或者它正在从数据库中删除,并且级联正在将其添加回来。不知道,我没查。请参阅下面的我的 cmets。

标签: java spring-boot jpa spring-data-jpa


【解决方案1】:

您的存储库设置不正确。您需要Registration 的复合 PK,并且您需要了解双向映射实际上仅用于查询。此外,CourseParticipate 中的双向映射存在挑战,因为通过Registration 实体的ManyToOne 关系默认为FetchType.EAGER。有了所有cascadefetch 注释,您就要求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;
}

是你的基本EntitiesRegistrationRepository 需要额外的查询。

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 在场景下所做的所有魔法:)
【解决方案2】:

OK 解决方案非常简单。

我确实需要从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 的情况下删除课程。

【讨论】:

    猜你喜欢
    • 2019-08-16
    • 2020-04-29
    • 1970-01-01
    • 2019-08-14
    • 2020-03-17
    • 2018-12-03
    • 2019-07-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多