简体   繁体   English

如何防止级联删除

[英]How to prevent cascading deletion

So I'm experimenting with Spring Data more specifically relationships (many to many).所以我正在试验 Spring 数据,更具体地说是关系(多对多)。

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: 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.从 output 可以看出,一切正常。 But I was wondering why the multithreading course got deleted depsite 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 .此外,我看到这种效果是因为在Student class 中,我使用的是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.当代码试图保存s1学生 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?我是否正确地假设我应该:保存多线程课程,然后获取多线程课程,然后将其添加到s1的课程列表中? But my confusion is when should I use cascading ALL and when should I not use it..但我的困惑是什么时候应该使用级联ALL,什么时候不应该使用它。

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 eg explained here: https://thorben-janssen.com/avoid-cascadetype-delete-many-assocations/一般来说, CascadeType.REMOVE / CascadeType.DELETE (包含在CascadeType.ALL中)不应该用于多对多关联,例如这里解释的: https://thorben-janssen.com/avoid-cascadetype-delete-多协会/

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 (eg 'Algorithm').Student上使用 CascadeType.REMOVE(包含在 ALL 中)时,通常会期望删除Student 6也会删除所有相关的Courses (例如“算法”)。
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 .在数据库中,这将导致外键约束违反,因为“算法” Course被其他Students引用,这会阻止级联删除并导致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.但是,如果整个测试在单个事务中运行(我猜您的测试方法或测试 class 用@Transactional注释?),更改会在事务结束时刷新,并且 hibernate 足够“聪明”,只能持久化仍然存在的实体及其协会。 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 .因此,插入了未删除的Students 、他们的相关Courses和关联,而数据库不知道删除了其他Students引用的Course

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).如果您将测试分成 2 个事务 - 一个保留所有实体及其关联,第二个删除学生 - 将发生提到的DataIntegrityViolationException (从业务角度代表常见情况,其中可能会删除学生比它的创建或更新晚得多)。

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:还要注意在关联它们之前保存新的(“瞬态”)实体(如果这不是级联的,正如您在删除所有 CascadeTypes 时发现的那样),并正确同步双向关联:

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM