1

I have a case where a participant can register courses.

Basically I have the following entity configuration (getters and setters omitted as well as other useless properties) :

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

}

Then I can from my viewmodel delete a registration or a course (I have also removed unnecessary stuff) :

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

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

Problem :

  • If I delete a registration, I need the @PreRemove function so I can remove the references. Without this the remove is ignored (no error, simply ignored)
  • If I delete a course, I have to remove the @PreRemove function else I get a ConcurrentModificationException (evidently...)

I also cannot remove references from the deleteRegistration method (instead of @PreRemove) because participant registrations are lazily loaded (would raise failed to lazily initialize a collection of role: ..., could not initialize proxy - no Session exception).

What is the best approach here ?

I use Java 11 with Spring Boot 1.0.4 (and spring-boot-starter-data-jpa).

EDIT :

The managers/repositories or defined this way (same for registration and participant) so it should be transactional (I don't have @EnableTransactionManagement on my main class but it should not be required as I don't use transactions outside of repositories) :

@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 :

I think I have found a pretty simple solution :

I have removed the @PreRemove method from the entity, then instead of removing the references like this in the deleteRegistration method (which I had tried but was causing failed to lazily initialize a collection of role exception) :

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

I simply set parents to null, I don't care as it will be deleted...

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

So now I can also delete a course without triggering the ConcurrentModificationException in the @PreRemove.

EDIT3 : My bad, registration was not removed with the solution above (still no error but nothing happens). I ended with this instead, which finally works :

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

No need to remove reference from participant...

Yann39
  • 14,285
  • 11
  • 56
  • 84
  • 1
    Why do you need `@PreRemove` to remove references? Shouldn't you be managing the entity graph within a transaction? – K.Nicholas Apr 26 '19 at 16:47
  • @K.Nicholas My managers **are** transactional (I edited the question to add the definition). I don't have much experience with JPA but it is not the first time I use it, and I never had this kind of problem in my other projects so there should be something special here. – Yann39 Apr 29 '19 at 08:12
  • My point/question is that it seems like you just want to remove children of an Entity. I really didn't read all this too close, but if that's the case then I think that `@PreRemove` is not meant for that purpose and so it doesn't surprise me that you are getting errors. If you want to remove children or relations of an entity you should be doing all that with individual deletes from the individual repos. – K.Nicholas Apr 29 '19 at 19:26
  • Thanks, I agree that `@PreRemove` may not be intended for that purpose, but this is the only way I have found to remove references in the collections. Doing it in the `deleteRegistration` method always raises `failed to lazily initialize a collection of roles` exception or do nothing. I have tried using `@EnableTransactionManagement` + `@Transactional` on method, calling `.size()` before removing, or even using `spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true`, none works, either I get the error or nothing happens. – Yann39 Apr 30 '19 at 08:17
  • You say when you delete a registration it is ignored with no error but I don't think so. I think it is being deleted and you are still seeing it in registrations and think it has not been deleted. Either that or it is being deleted from the database and the cascade is adding it back. I don't know, I didn't check. See my comments below. – K.Nicholas May 01 '19 at 05:38
  • I had enabled JPA tracing and could see that no `DELETE` query was issued, record in DB is still there and ID did not change. It's like it is totally ignored. There can be multiple causes according to what I read (like [this](https://stackoverflow.com/questions/26349213/hibernate-entitymanager-remove-referenced-entity-not-working) or [this](https://stackoverflow.com/a/16901857/1274485) or [this](https://stackoverflow.com/a/20582674/1274485), ...) but nothing really applies to my case. Anyway I have found a pretty simple solution, see edit2 in the question. Thank you very much for your time! – Yann39 May 01 '19 at 12:00

2 Answers2

1

You have setup your repositories incorrectly. You need a composite PK for Registration and you need to understand that bidirectional mappings are really for query only. Further, bidirectional mappings in Course and Participate present challenges because the ManyToOne relationship through the Registration entity is FetchType.EAGER by default. With all the cascade and fetch annotations you have you are asking for a complicated combination of things from JPA and it seems like you really haven't sorted it all out yet. Start with the basics, be sure to print your SQL statements, and proceed from there if you want to try to finesse more from 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;
}

Is your basic Entities. The RegistrationRepository needs an additional query.

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

And to use all this in an example:

@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);
}
K.Nicholas
  • 10,956
  • 4
  • 46
  • 66
  • 1
    Could be a solution, but I want to avoid modifying the core structure of my db/application, which currently runs in production (I have just been asked to add the delete course functionality). And bidirectional mappings are very convenient to work with in the code (i.e. I very often have to display course registrations) so I want to keep them. Anyway thanks for this different approach, I still have to learn about all the magic JPA is doing under the scene :) – Yann39 May 01 '19 at 12:10
0

OK solution was pretty simple.

I indeed need to remove the references from the deleteRegistration method. This is what I had tried but was causing failed to lazily initialize a collection of role exception :

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

The trick is that I also have to save the course entity before trying to delete the registration.

This works :

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

No need to remove reference from participant...

@PreRemove was doing the job, but that way I can now also delete a course without triggering the ConcurrentModificationException.

Yann39
  • 14,285
  • 11
  • 56
  • 84