多对多关系

多对多关系的判断方式是,站在两张表无论哪一张表的角度上,其中的一条数据都对应另外一张表的多个数据,就是多对多关系。现实中比较典型的关系是学生与课程的关系,一个学生会选多门课程,一门课程会有多个学生上课。

多对多关系显然无法用两张表互相外键关联来实现,在实践中的多对多关系是通过一张中间表,将两张数据表的id进行互相对应来实现的。这张中间表实际上有两个外键各自关联到两张表的id上(当然,多对多不仅局限于两张表,更多表的多对多关系也可以,但是一般不会采用这么复杂的业务逻辑)

我们现在就新增一个student表和一个中间表来为课程与学生的多对多关系做准备。

CREATE TABLE `student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) DEFAULT NULL,
  `last_name` varchar(45) DEFAULT NULL,
  `email` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;


CREATE TABLE `course_student` (
  `course_id` int(11) NOT NULL,
  `student_id` int(11) NOT NULL,

  PRIMARY KEY (`course_id`,`student_id`),

  KEY `FK_STUDENT_idx` (`student_id`),

  CONSTRAINT `FK_COURSE_05` FOREIGN KEY (`course_id`)
  REFERENCES `course` (`id`)
  ON DELETE NO ACTION ON UPDATE NO ACTION,

  CONSTRAINT `FK_STUDENT` FOREIGN KEY (`student_id`)
  REFERENCES `student` (`id`)
  ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

可以看到新建了一个简单的学生表,然后创建了一个中间表course_student,其中两个外键分别关联到course表和student表。

操作多对多关系

一对多和一对一关系通过外键在哪一侧和操作哪个对象来区分Bi和Uni方式,而多对多就没有区分这两种关系了,在任何一侧应该都要能查到另一侧的关联数据,先来更新Course类:

@ManyToMany(cascade = {CascadeType.DETACH, CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
@JoinTable(name = "course_student",
        joinColumns = @JoinColumn(name = "course_id"),
        inverseJoinColumns = @JoinColumn(name = "student_id"))
private List<Student> students;

public void addStudent(Student student) {
    if (students == null) {
        students = new ArrayList<>();
    }
    students.add(student);
}

首先我们知道,肯定是取多个学生对象,所以是一个List泛型。然后就是注解@ManyToMany,以及下边关键的@JoinTable注解。

@JoinTable注解中的表名设置的是中间表的名称,然后joinColumns = @JoinColumn(name = "course_id")表示中间表中链接到自己所在的表的外键列是course_id

inverseJoinColumns = @JoinColumn(name = "student_id")则表示inverse那一方的外键列是student_id,其对应的类是List泛型中的Student类。

在数据库中,对于这种多对多关系,我们现在所在的类是Course,就称Student类为inverse,也就是另外一方的意思。

由于多对多不区分Bi和Uni,很容易想到,Student类里也要如此设置,只不过次序正好相反。来创建Student类:

import javax.persistence.*;
import java.util.List;

@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "email")
    private String email;

    @ManyToMany(cascade = {CascadeType.DETACH, CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
    @JoinTable(name = "course_student",
            joinColumns = @JoinColumn(name = "student_id"),
            inverseJoinColumns = @JoinColumn(name = "course_id"))
    private List<Course> courses;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Student() {

    }

    public Student(String firstName, String lastName, String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", email='" + email + '\'' +
                '}';
    }

    public void addCourse(Course course) {
        if (courses == null) {
            courses = new ArrayList<>();
        }
        courses.add(course);
    }
}

可见Student类中的外键属性表名不变,把两个外键倒换了一下位置,因为从Student类来说,另外一方的类是Course类。

还要注意设置多对多的级联关系,很显然,删除一个课程不能把对应的学生对象全部删除掉,而删除学生也不能把课程删除掉,因为都会影响其他数据。

还有需要注意的,就是Course.addStudent(Student student)Student.addCourse(Course course)方法,这两个方法只能将对方添加进来,不要去调用对方的方法,否则会无限循环互相添加。

然后就可以来编写操作代码了,先创建一些学生对象,然后设置好与取得的course对象的关系,之后写入数据库:

public class MainApp {

    public static void main(String[] args) {
        SessionFactory factory = new Configuration().configure("hibernate.cfg.xml").addAnnotatedClass(Instructor.class).addAnnotatedClass(InstructorDetail.class).addAnnotatedClass(Course.class).addAnnotatedClass(Review.class).addAnnotatedClass(Student.class).buildSessionFactory();
        Session session = factory.getCurrentSession();
        Random rand = new Random();
        try {
            session.beginTransaction();
            //获取随机Course对象,给Course对象添加student
            Course course = session.get(Course.class, rand.nextInt(41) + 1);
            for (int k = 0; k < rand.nextInt(10) + 1; k++) {
                Student student = new Student(getRandomString(), getRandomString(), getRandomString());
                //这里由于新建对象处于transient,状态,没有与session关联,需要先关联一下
                session.save(student);
                //关联之后,再将student添加到course中。
                course.addStudent(student);
            }

            session.getTransaction().commit();
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            session.close();
            factory.close();
        }

    }
//
    public static String getRandomString() {
        String base = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
        Random rand = new Random();
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < 6; i++) {
            stringBuffer.append(base.charAt(rand.nextInt(62)));
        }
        return stringBuffer.toString();
    }
}

在新建了Student对象之后,需要进行save一下,否则无法直接与Course进行关联,在保存的时候也会出错。如果Course也是新建对象,也需要先save一下。之后把student对象设置到course中,这样就完成了一个course对应多个Student的操作。

如果一个学生选了多门课,就反过来写,将course关联给student对象:

try {
    session.beginTransaction();
    //模拟学生选课,随机选一个学生对象,然后随机选三门课
    Student student = session.get(Student.class, rand.nextInt(58) + 1);
    for (int j = 0; j < rand.nextInt(5) + 1; j++) {
        Course course1 = session.get(Course.class, rand.nextInt(41) + 1);
        student.addCourse(course1);
    }

    session.getTransaction().commit();
} catch (Exception ex) {
    ex.printStackTrace();
} finally {
    session.close();
    factory.close();
}

注意,这里采用了随机数选出三个课程然后设置给student对象,如果三个课程里有重复的话,会报Duplicate entry错误。实际开发中肯定需要获得确定的Course id才可以,虽然也可以编写一个产生不同数字的函数,不过懒得写了。

然后是删除的部分,注意,刚才已经说了,根据现实情况,不能级联删除course和student表里的数据,只是清除对应关系,也就是清除中间表的内容。

现在course_student表里有id为22的course和41的student对象有关联,来删除课程看看:

//删除课程和关联关系
Course course = session.get(Course.class, 22);
session.delete(course);

很简单,直接删除就可以,Hibernate会同时自动到中间表里删除这个课程的id对应的记录。删除学生的操作也是一样:

//删除学生和关联关系
Student student = session.get(Student.class, 49);
session.delete(student);

只要不设置级联删除,删除都只影响本来的对象和中间表。

查询就很简单了,只要获取了对象之后显示就可以了。

Course course = session.get(Course.class, 9);
for (Student s : course.getStudents()) {
    System.out.println(s);
}
//模拟通过学生查课程
Student student = session.get(Student.class, 53);
for (Course c : student.getCourses()) {
    System.out.println(c);
}

至此Hibernate的基本操作结束了,其实Hibernate远不止这些内容,每一个大版本更新也有很多变化,以后再通过《Java Persistence with Hibernate, Second Edition》学习吧。