29

I have 3 tables in my database: Students, Courses and Students_Courses

Students can have multiple courses and courses can have multiple students. There is a many-to-many relationship between Students and Courses.

I have 3 cases for my project and courses added to my Courses table.

  • (a) When I add a user, it gets saved fine,
  • (b) When I add courses for the student, it creates new rows in User_Courses - again, expected behaviour.
  • (c) When I am trying to delete the student, it is deleting the appropriate records in Students and Students_Courses, but it is also deleting Courses records which is not required. Even if I don't have any user in a course, I want the course to be there.

Below is my code for tables and annotate classes.

    CREATE TABLE `Students` (
    `StudentID` INT(11) NOT NULL AUTO_INCREMENT,
    `StudentName` VARCHAR(50) NOT NULL 
    PRIMARY KEY (`StudentID`)
)

CREATE TABLE `Courses` (
    `CourseID` INT(11) NOT NULL AUTO_INCREMENT,
    `CourseName` VARCHAR(50) NOT NULL 
    PRIMARY KEY (`CourseID`)
)

CREATE TABLE `Student_Courses` (
    `StudentId` INT(10) NOT NULL DEFAULT '0',
    `CourseID` INT(10) NOT NULL DEFAULT '0',
    PRIMARY KEY (`StudentId`, `CourseID`),
    INDEX `FK__courses` (`CourseID`),
    INDEX `StudentId` (`StudentId`),
    CONSTRAINT `FK__courses` FOREIGN KEY (`CourseID`) REFERENCES `courses` (`CourseID`) ON DELETE NO ACTION,
    CONSTRAINT `FK_students` FOREIGN KEY (`StudentId`) REFERENCES `students` (`StudentId`)
)

This is the Java code generated by Hibernate:

@Entity
@Table(name = "Students")
public class Students implements java.io.Serializable {

    private Integer StudentID;
     private String Students;
    private Set<Courses> Courseses = new HashSet<Courses>(0);

    public Students() {
    }


    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "StudentID", unique = true, nullable = false)
    public Integer getStudentID() {
        return this.StudentID;
    }

    public void setStudentID(Integer StudentID) {
        this.StudentID = StudentID;
    }

    @Column(name = "Students", nullable = false, length = 50)
    public String getCampaign() {
        return this.Students;
    }

    public void setCampaign(String Students) {
        this.Students = Students;
    }


 @ManyToMany(cascade = {CascadeType.ALL}, fetch = FetchType.LAZY)
    @JoinTable(name = "Student_Courses", joinColumns = {
        @JoinColumn(name = "StudentId", nullable = false, updatable = false)}, inverseJoinColumns = {
        @JoinColumn(name = "CourseID", nullable = false, updatable = false)})
    public Set<Courses> getCourseses() {
        return this.Courseses;
    }

     public void setCourseses(Set<Courses> Courseses) {
        this.Courseses = Courseses;
    }

    }


    @Entity
@Table(name = "Courses")
public class Courses implements java.io.Serializable {

  private Integer CourseID;
    private String CourseName;
     private Set<Students> Studentses = new HashSet<Students>(0);

    public Courses() {
    }

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "CourseID", unique = true, nullable = false)
    public Integer getCourseID() {
        return this.CourseID;
    }

    public void setCourseID(Integer CourseID) {
        this.CourseID = CourseID;
    }

     @Column(name = "CourseName", nullable = false, length = 100)
    public String getCourseName() {
        return this.CourseName;
    }

    public void setCourseName(String CourseName) {
        this.CourseName = CourseName;
    }

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "Courseses")
    public Set<Students> getStudentses() {
        return this.Studentses;
    }

    public void setStudentses(Set<Students> Studentses) {
        this.Studentses = Studentses;
    }

    }

How can I achieve what I have described? I could not find any reasonable documentation on the web.

J0e3gan
  • 8,740
  • 10
  • 53
  • 80
Sushant Gupta
  • 1,487
  • 2
  • 16
  • 23
  • 2
    `FetchType.Lazy` is the default value for the `@ManyToMany` annotation; so you could delete it. – herau Jul 01 '15 at 07:46

3 Answers3

38

I found the correct mapping (and tested that with JUnit with an extensive case) in a similar scenario. I don't think I am going to post testing code because it would take long time to adapt to this example. Anyway the key is to:

  • Not use mappedBy attribute for the annotations, use join columns
  • List the possible CascadeTypes excluding REMOVE

In OP's example

@ManyToMany(fetch = FetchType.LAZY,
        cascade =
        {
                CascadeType.DETACH,
                CascadeType.MERGE,
                CascadeType.REFRESH,
                CascadeType.PERSIST
        },
        targetEntity = Course.class)
@JoinTable(name = "XTB_STUDENTS_COURSES",
        inverseJoinColumns = @JoinColumn(name = "COURSE_ID",
                nullable = false,
                updatable = false),
        joinColumns = @JoinColumn(name = "STUDENT_ID",
                nullable = false,
                updatable = false),
        foreignKey = @ForeignKey(ConstraintMode.CONSTRAINT),
        inverseForeignKey = @ForeignKey(ConstraintMode.CONSTRAINT))
private final Set<Course> courses = new HashSet<>();

@ManyToMany(fetch = FetchType.LAZY,
        cascade =
        {
                CascadeType.DETACH,
                CascadeType.MERGE,
                CascadeType.REFRESH,
                CascadeType.PERSIST
        },
        targetEntity = Student.class)
@JoinTable(name = "XTB_STUDENTS_COURSES",
        joinColumns = @JoinColumn(name = "COURSE_ID",
                nullable = false,
                updatable = false),
        inverseJoinColumns = @JoinColumn(name = "STUDENT_ID",
                nullable = false,
                updatable = false),
        foreignKey = @ForeignKey(ConstraintMode.CONSTRAINT),
        inverseForeignKey = @ForeignKey(ConstraintMode.CONSTRAINT))
private final Set<Student> students = new HashSet<>();

Extensive JUnit testing verified that:

  • I can add courses to students and vice versa flawlessly
  • If I remove a course from a student, the course is not deleted
  • Vice versa
  • If I remove a student, all courses are detached but they are still persisted (to other students) in database
  • Vice versa
usr-local-ΕΨΗΕΛΩΝ
  • 26,101
  • 30
  • 154
  • 305
  • What if all the `Students` are deleted, would it delete `Courses` in this case or just deletes the records from the associated (`student_courses`) table? – being_j Aug 01 '18 at 15:05
  • The meaning is that an empty course shall not be deleted. If you want to mandate that courses must have at least one student, you can delete Course as soon as you drop its last student – usr-local-ΕΨΗΕΛΩΝ Aug 01 '18 at 17:31
  • @ usr-local-ΕΨΗΕΛΩΝ Here, is it really needed to use "@JoinTable" annotation on both the entity Student and Course. I think, we need to annotate "@JointTable" either on Course or on Student. That should work fine. – Gunjan Shah Apr 12 '23 at 15:02
18

Based on what you've told me you don't want cascade=CascadeType.ALL on the getCourseses method in Student. Keep in mind that Hibernate cascades are not the same as database cascades. Even if you don't have any cascades then Hibernate will delete the Students_Courses record.

The best way to think of Hibernate cascades is that if you call an operation on an entity and that operation is listed in the cascade list then that operation will be called on all of the child entities.

For example, when you call delete on Student, since delete is in the cascade list for Courses, Hibernate will call delete on each of the Course entities referenced by that student. That is why you are seeing the Course records disappearing.

Don't worry about database cascades, Hibernate will take care of those on its own.

Pace
  • 41,875
  • 13
  • 113
  • 156
  • 1
    Yes, using `cascade=ALL` is not the best solution. However the answer does not provide the correct answer for deleting the bridging record and not the Course record. And `I'm having the same problem, too` :-( – usr-local-ΕΨΗΕΛΩΝ Sep 02 '15 at 10:37
  • 1
    If you want to delete the bridging record then you need to remove the entity from the owning side's collection. The owning side is the side that does not have the mappedBy attribute. In the above example you would want to remove the corresponding Courses instance from the appropriate Students instance. – Pace Sep 03 '15 at 19:10
  • Great, I also found out the correct mapping involves listing the supported cascade types. In my case I listed them all but remove – usr-local-ΕΨΗΕΛΩΝ Sep 04 '15 at 07:53
3

You just need to Remove cascade = CascadeType.ALL in Student class only no change is required in Courses class

and add the below code cascade = {CascadeType.PERSIST,CascadeType.MERGE,CascadeType.DETACH}..

It means while deleting owner class record it will not delete a non-owner record.

After this, On Delete it will delete only from Student table and student_course. course table data remains the same.