How to prevent cascading deletion

Viewed 85

So I'm experimenting with Spring Data more specifically relationships (many to many).

This is what I have at the moment:

COURSE:

@Entity
@Table(name = "COURSE")
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer courseId;

    @Column
    private String name;

    @ManyToMany(mappedBy = "courses", fetch = FetchType.EAGER)
    List<Student> students;

    public Course(String name) {
        this.name = name;
        //this.department = department;
    }

    protected Course() {
    }

    public Integer getId() {
        return courseId;
    }

    public String getName() {
        return name;
    }

    public List<Student> getStudents(){ return students;}

    @Override
    public String toString() {
        return "Course{" +
                "id=" + courseId + ", name='" + name + '\'';
    }
}

STUDENT:

@Entity
@Table(name = "STUDENT")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer studentId;

    @Column
    private boolean fullTime;

    @Column
    private Integer age;

    @Embedded
    private Person attendee;

    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinTable(name = "students_courses",
    joinColumns = @JoinColumn(name = "student_Id", referencedColumnName = "studentId"),
    inverseJoinColumns = @JoinColumn(name = "course_Id", referencedColumnName = "courseId"))
    private List<Course> courses;// = new ArrayList<>();

    public Student(Person attendee, boolean fullTime, Integer age) {
        this.attendee = attendee;
        this.fullTime = fullTime;
        this.age = age;
        courses = new ArrayList<>();
    }

    protected Student() {
    }

    public Integer getStudentId() {
        return studentId;
    }

    public Person getAttendee() {
        return attendee;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public boolean isFullTime() {
        return fullTime;
    }

    public Integer getAge() {
        return age;
    }

    public List<Course> getCourses() {
        return courses;
    }

    @Override
    public String toString() {
        StringBuilder courseStr = new StringBuilder();
        getCourses().forEach(course -> courseStr.append(course.getName()));
        return "Student{" + "studentId=" + studentId + ", " + attendee + ", fullTime=" + fullTime +
                ", age=" + age + " , course=" + courseStr.toString() +"}\n";
    }
}

My test case:

@Test
public void simpleStudentCrudExample() {
    boolean fullTime = true;
    studentRepository.save(new Student(new Person("jane", "doe"), fullTime, 20));
    studentRepository.save(new Student(new Person("john", "doe"), fullTime, 22));
    studentRepository.save(new Student(new Person("mike", "smith"), fullTime, 18));
    studentRepository.save(new Student(new Person("ally", "kim"), !fullTime, 19));
    studentRepository.save(new Student(new Person("ally", "kim"), !fullTime, 19));
    Student s1 = new Student(new Person("Bob", "Cho"), fullTime, 26);
    s1.getCourses().add(new Course("Multithreading"));
    studentRepository.save(s1);

    createCourse();

    System.out.println("\n*************Printing Original Students*************");
    studentRepository.findAll().forEach(System.out::println);

    System.out.println("\n*************Printing Courses*************");
    courseRepository.findAll().forEach(System.out::println);
    List<Course> courseList = courseRepository.findAll();

    //age up the students and add course
    studentRepository.findAll().forEach(student -> {
        List<Course> collect = courseRepository.findAll().stream()
                .filter(course2 -> course2.getId() != 1)
                .collect(Collectors.toList());
        student.setAge(student.getAge() + 1);
        student.getCourses().addAll(collect);
        Student save = studentRepository.save(student);
    });

    System.out.println("\n*************Students a year older and course added*************");
    for (Student student : studentRepository.findAll()) {
        System.out.println(student);
    }

    studentRepository.deleteById(6);

    System.out.println("\n*************Deleted student by id 6*************");
    for (Student student : studentRepository.findAll()) {
        System.out.println(student);
    }
    System.out.println("\n*************Printing Courses*************");
    courseRepository.findAll().forEach(System.out::println);
}

private void createCourse() {
    course = new Course("Algorithm");//, department);
    course1 = new Course("Databases");//, department);

    courseRepository.save(course);
    courseRepository.save(course1);
}

Output:

*************Printing Original Students*************
2021-12-12 13:09:37.297  INFO 12484 --- [           main] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory
Student{studentId=1,  firstName='jane', lastname='doe' , fullTime=true, age=20 , course=}

Student{studentId=2,  firstName='john', lastname='doe' , fullTime=true, age=22 , course=}

Student{studentId=3,  firstName='mike', lastname='smith' , fullTime=true, age=18 , course=}

Student{studentId=4,  firstName='ally', lastname='kim' , fullTime=false, age=19 , course=}

Student{studentId=5,  firstName='ally', lastname='kim' , fullTime=false, age=19 , course=}

Student{studentId=6,  firstName='Bob', lastname='Cho' , fullTime=true, age=26 , course=Multithreading}


*************Printing Courses*************
Course{id=1, name='Multithreading'
Course{id=2, name='Algorithm'
Course{id=3, name='Databases'

*************Students a year older and course added*************
Student{studentId=1,  firstName='jane', lastname='doe' , fullTime=true, age=21 , course=AlgorithmDatabases}

Student{studentId=2,  firstName='john', lastname='doe' , fullTime=true, age=23 , course=AlgorithmDatabases}

Student{studentId=3,  firstName='mike', lastname='smith' , fullTime=true, age=19 , course=AlgorithmDatabases}

Student{studentId=4,  firstName='ally', lastname='kim' , fullTime=false, age=20 , course=AlgorithmDatabases}

Student{studentId=5,  firstName='ally', lastname='kim' , fullTime=false, age=20 , course=AlgorithmDatabases}

Student{studentId=6,  firstName='Bob', lastname='Cho' , fullTime=true, age=27 , course=MultithreadingAlgorithmDatabases}


*************Deleted student by id 6*************
Student{studentId=1,  firstName='jane', lastname='doe' , fullTime=true, age=21 , course=AlgorithmDatabases}

Student{studentId=2,  firstName='john', lastname='doe' , fullTime=true, age=23 , course=AlgorithmDatabases}

Student{studentId=3,  firstName='mike', lastname='smith' , fullTime=true, age=19 , course=AlgorithmDatabases}

Student{studentId=4,  firstName='ally', lastname='kim' , fullTime=false, age=20 , course=AlgorithmDatabases}

Student{studentId=5,  firstName='ally', lastname='kim' , fullTime=false, age=20 , course=AlgorithmDatabases}


*************Printing Courses*************
Course{id=2, name='Algorithm'
Course{id=3, name='Databases'

As you can see from the output, everything works. But I was wondering why the multithreading course got deleted despite only deleting the student that was associated to it. From a database perspective, I was under the impression that it would only delete the targeted entity/record and not anything it's related to (in this case; the multithreading course). Am I right with this assumption? Is it normal from a database perspective to delete records that are related to the record you are deleting?

Furthermore, I am seeing this effect because in the Student class, I am using cascade = CascadeType.ALL. If I delete this then I get the following error:

org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.example.university.domain.Course; nested exception is java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.example.university.domain.Course

This occurs when the code is trying to save the s1 student object. Am I right in assuming that I should: save the multithreading course, then fetch the multithreading course and then add it to s1's course list? But my confusion is when should I use cascading ALL and when should I not use it.

1 Answers

This question is interesting due to the fact that the provided test seems to prove that everything works fine.

In general, CascadeType.REMOVE/CascadeType.DELETE (included in CascadeType.ALL) should never be used for many-to-many associations, as e.g. explained here: https://thorben-janssen.com/avoid-cascadetype-delete-many-assocations/

When using CascadeType.REMOVE (included in ALL) on the Student, one would normally expect that the deletion of Student 6 would also delete all associated Courses (e.g. 'Algorithm').
At the database this would then lead to a foreign key constraint violation, since the 'Algorithm' Course is referenced by other Students, which prevents the cascaded deletion and causes a DataIntegrityViolationException.

However, in case the whole test runs in a single transaction (I guess your test method or test class is annotated with @Transactional ?), the changes are flushed at the end of the transaction and hibernate is "clever" enough to only persist the still existing entities and their associations. Consequently, the non-deleted Students, their associated Courses and the associations are inserted, without the database ever knowing about the deletion of a Course which is referenced by other Students.

If you would separate your test into 2 transactions - one persisting all the entities and their associations, and a second one deleting the student - the mentioned DataIntegrityViolationException would occur (representing the common case from a business perspective, where the deletion of a student might happen much later than it's creation or update).

Also be aware to save new ("transient") entities before associating them (in case this is not cascaded, as you discovered when removing all CascadeTypes), and to properly synchronize bi-directional associations:

Related