关于 Hibernate - 多对多
这是关于 Hibernate 多对多映射的笔记。
引言
这是关于 Hibernate 多对多映射的笔记。
背景
在我之前的笔记中,我讨论了 Hibernate 中的一对多映射。在本笔记中,我将讨论多对多映射。

student 和 course 关系是一个典型的多对多关系。以下情况在所有多对多关系中非常常见。
- student在注册任何- course之前,先在- school注册。因此,- student在- [Student]表中存在,然后才会在- [Student_Course]表中有相应的条目。
- course由- school设计,然后才有任何注册的- student。因此,- course在- [Course]表中存在,然后才会在- [Student_Course]表中有相应的条目。
- 多对多关系最常见的操作只是在 [Student_Course]中插入和删除行,以显示student是否注册或退出了course。
本笔记旨在展示如何在 Hibernate 中操作多对多关系的示例。
数据库
与我之前的笔记一样,我使用 MySQL 数据库。如果您不熟悉 MySQL,您可以查看我之前的笔记。在本笔记中,我在 Linux Mint 机器上使用了 5.5.62-0ubuntu0.14.04.1 版本。您可以执行以下脚本来创建数据库和表。
DROP DATABASE IF EXISTS experimentB;
CREATE DATABASE experimentB;
    
USE experimentB;
    
CREATE TABLE `Student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `Name` varchar(100) CHARACTER SET utf8 NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
    
CREATE TABLE `Course` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) CHARACTER SET utf8 NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
    
CREATE TABLE `Student_Course` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `studentId` int(11) NOT NULL,
  `courseId` int(11) NOT NULL,
  `score` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_constraint` (`studentId`,`courseId`),
  KEY `fk_Student_Course_Student_idx` (`studentId`),
  KEY `fk_Student_Course_Course_idx` (`courseId`),
  CONSTRAINT `fk_Student_Course_Course` FOREIGN KEY (`courseId`)
    REFERENCES `Course` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
  CONSTRAINT `fk_Student_Course_Student` FOREIGN KEY (`studentId`)
    REFERENCES `Student` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;

实体和 Hibernate 配置
附件是一个 Maven 项目。它的结构与之前的笔记完全相同。

以下类将表映射到实体。
package com.song.example.hibernate.entities;
    
import java.util.ArrayList;
import java.util.List;
    
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
    
@Entity
@Table(name = "Student")
public class Student {
    private Integer id;
    private String name;
    private List<StudentCourse> studentCourses = new ArrayList<StudentCourse>();
    
    public Student() {}
    public Student(String name) { this.name = name; }
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "Id")
    public Integer getId() { return this.id; }
    public void setId(Integer id) { this.id = id; }
    
    @Column(name = "Name", length = 100)
    public String getName() { return this.name; }
    public void setName(String name) { this.name = name; }
    
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "student")
    public List<StudentCourse> getStudentCourses() { return this.studentCourses; }
    public void setStudentCourses(List<StudentCourse> studentCourses) { 
        this.studentCourses = studentCourses; 
    }
}
package com.song.example.hibernate.entities;
    
import java.util.ArrayList;
import java.util.List;
    
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
    
@Entity
@Table(name = "Course")
public class Course {
    private Integer id;
    private String name;
    private List<StudentCourse> studentCourses = new ArrayList<StudentCourse>();
    
    public Course() {}
    public Course(String name) { this.name = name; }
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "Id")
    public Integer getId() { return this.id; }
    public void setId(Integer id) { this.id = id; }
    
    @Column(name = "Name", length = 100)
    public String getName() { return this.name; }
    public void setName(String name) { this.name = name; }
    
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "course")
    public List<StudentCourse> getStudentCourses() { return this.studentCourses; }
    public void setStudentCourses(List<StudentCourse> studentCourses) {
        this.studentCourses = studentCourses; 
    }
}
package com.song.example.hibernate.entities;
    
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
    
@Entity
@Table(name = "Student_Course")
public class StudentCourse {
    private Integer id;
    private Integer score;
    
    private Student student;
    private Course course;
    
    public StudentCourse() {}
    public StudentCourse(Student student, Course course) {
        this.student = student;
        this.course = course;
    }
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "Id")
    public Integer getId() { return this.id; }
    public void setId(Integer id) { this.id = id; }
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "studentId", nullable = false)
    public Student getStudent() { return this.student; }
    public void setStudent(Student student) { this.student = student; }
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "courseId", nullable = false)
    public Course getCourse() { return this.course; }
    public void setCourse(Course course) { this.course = course; }
    
    @Column(name = "score", length = 100)
    public Integer getScore() { return this.score; }
    public void setScore(Integer score) { this.score = score; }
}
Hibernate 配置添加到 hibernate.cfg.xml 文件中。
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
        
<hibernate-configuration>
    <session-factory>
        <property name="connection.driver_class">
            com.mysql.cj.jdbc.Driver
        </property>
        <property name="connection.url">
            jdbc:mysql:///experimentB
        </property>
        <property name="connection.username">root</property>
        <property name="connection.password">password</property>
    
        <property name="connection.pool_size">100</property>
        <property name="dialect">
            org.hibernate.dialect.MySQLDialect
        </property>
    
        <property name="cache.provider_class">
            org.hibernate.cache.NoCacheProvider
        </property>
        
        <property name="org.hibernate.flushMode">MANUAL</property>
    
        <property name="show_sql">false</property>
        <property name="format_sql">false</property>
        
        <mapping class="com.song.example.hibernate.entities.Student" />
        <mapping class="com.song.example.hibernate.entities.Course" />
        <mapping class="com.song.example.hibernate.entities.StudentCourse" />
        
    </session-factory>
</hibernate-configuration>
在 hibernate.cfg.xml 文件中,我添加了用户名和密码条目。如果要运行该示例,您需要确保用户名具有操作表的必要权限。
单元测试
在本笔记中,我使用 TestNG 运行单元测试。所有测试都写在 SC_Test.java 文件中。
package com.song.example.hibernate;
    
import java.sql.Statement;
import java.util.List;
    
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.testng.Assert;
import org.testng.Reporter;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
    
import com.song.example.hibernate.entities.Course;
import com.song.example.hibernate.entities.Student;
import com.song.example.hibernate.entities.StudentCourse;
    
public class SC_Test {
    private final SessionFactory sessionFactory
        = SessionFactoryInstance.Instance;
    
    @BeforeTest
    public void Init() {
        final String test_name = "Init()";
        Reporter.log(test_name, true);
        
        try (Session session = sessionFactory.openSession()) {
            FlushMode flushmode = session.getHibernateFlushMode();
            Assert.assertEquals(flushmode, FlushMode.MANUAL);
            
            session.doWork(connection -> {
    
                try (Statement statement = connection.createStatement()) {
                    statement.execute("SET FOREIGN_KEY_CHECKS = 0;");
                    statement.execute("TRUNCATE TABLE Student_Course;");
                    statement.execute("TRUNCATE TABLE Student;");
                    statement.execute("TRUNCATE TABLE Course;");
                    statement.execute("SET FOREIGN_KEY_CHECKS = 1;");
                }
            });
        }
        
        try (Session session = sessionFactory.openSession()) {
            Transaction tx = session.getTransaction();
            
            try {
                tx.begin();
                session.save(new Student("Student No.1"));
                session.save(new Student("Student No.2"));
                session.save(new Course("Course No.1"));
                session.save(new Course("Course No.2"));
                session.flush();
                tx.commit();
            }
            catch(Exception e) {
                tx.rollback();
                Assert.fail(test_name + " - Add students and courses");
            }
        }
        
        // Validate students and courses are added
        try (Session session = sessionFactory.openSession()) {
            
            List<Student> students = session
                    .createQuery("SELECT s FROM Student s", Student.class)
                    .getResultList();
            List<Course> courses = session
                    .createQuery("SELECT c FROM Course c", Course.class)
                    .getResultList();
            
            Assert.assertEquals(students.size(), 2);
            Assert.assertEquals(courses.size(), 2);
        }
    }
    
    @Test
    public void Add_Student_To_Course_Test() {
        final String test_name = "Assign_Student_To_Course_Test()";
        Reporter.log(test_name, true);
        
        final int studentId = 1, courseId = 1;
        final String courseName = "Course No.1";
        
        try (Session session = sessionFactory.openSession()) {
            Transaction tx = session.getTransaction();
            
            try {
                tx.begin();
                Student student = session.get(Student.class, studentId);
                Course course = session.get(Course.class, courseId);
                
                StudentCourse studentCourse = new StudentCourse(student, course);
                student.getStudentCourses().add(studentCourse);
                session.save(studentCourse);
                
                session.flush();
                tx.commit();
            } catch(Exception e) {
                tx.rollback();
                Assert.fail(test_name + " - Failed to commit the changes");
            }
        }
        
        // Validate the student assigned to the course
        try (Session session = sessionFactory.openSession()) {
            Student student = session.get(Student.class, studentId);
            
            List<StudentCourse> studentCourses = student.getStudentCourses();
            Assert.assertEquals(studentCourses.size(), 1);
            
            StudentCourse studentCourse = studentCourses.get(0);
            Assert.assertNull(studentCourse.getScore(),
                    test_name + " - score should not have been assigned");
            
            Course course = studentCourse.getCourse();
            Assert.assertEquals(course.getName(), courseName);
        }
    }
    
    @Test(dependsOnMethods = {"Add_Student_To_Course_Test"})
    public void Assign_Score_To_Student_Test() {
        final String test_name = "Assign_Score_To_Student_Test()";
        Reporter.log(test_name, true);
        
        final int studentId = 1, score = 95;
        
        try (Session session = sessionFactory.openSession()) {
            Transaction tx = session.getTransaction();
            
            try {
                tx.begin();
                Student student = session.get(Student.class, studentId);
                StudentCourse studentCourse = student.getStudentCourses().get(0);
                
                studentCourse.setScore(score);
                session.flush();
                tx.commit();
            } catch(Exception e) {
                tx.rollback();
                Assert.fail(test_name + " - Failed to commit the changes");
            }
        }
        
        // Validate the score updated
        try (Session session = sessionFactory.openSession()) {
            Student student = session.get(Student.class, studentId);
            
            List<StudentCourse> studentCourses = student.getStudentCourses();
            StudentCourse studentCourse = studentCourses.get(0);
            Assert.assertEquals(studentCourse.getScore().intValue(), score);
        }
    }
    
    @Test(dependsOnMethods = {"Assign_Score_To_Student_Test"})
    public void Drop_And_Add_Student_To_Course_Test() {
        final String test_name = "Drop_And_Add_Student_To_Course_Test()";
        Reporter.log(test_name, true);
        
        final int studentId = 1, courseId = 2, score = 100;
        final String courseName = "Course No.2";
        
        try (Session session = sessionFactory.openSession()) {
            Transaction tx = session.getTransaction();
            
            try {
                tx.begin();
                Student student = session.get(Student.class, studentId);
                List<StudentCourse> studentCourses = student.getStudentCourses();
                
                StudentCourse existingCourse = studentCourses.get(0);
                studentCourses.remove(existingCourse);
                
                Course course = session.get(Course.class, courseId);
                StudentCourse newCourse = new StudentCourse(student, course);
                newCourse.setScore(score);
                studentCourses.add(newCourse);
                
                session.delete(existingCourse);
                session.save(newCourse);
                
                session.flush();
                tx.commit();
            } catch(Exception e) {
                tx.rollback();
                Assert.fail(test_name + " - Failed to commit the changes");
            }
        }
        
        // Validate course dropped and added
        try (Session session = sessionFactory.openSession()) {
            Student student = session.get(Student.class, studentId);
            
            List<StudentCourse> studentCourses = student.getStudentCourses();
            Assert.assertEquals(studentCourses.size(), 1);
            
            StudentCourse studentCourse = studentCourses.get(0);
            Assert.assertEquals(studentCourse.getScore().intValue(), score);
            
            Assert.assertEquals(studentCourse.getCourse().getName(), courseName);
        }
    }
}
- public void Init()- 在我们开始测试之前,在数据库中添加了 2 个- student和 2 个- course。
- public void Add_Student_To_Course_Test()- 此测试将- student注册到- course。
- public void Assign_Score_To_Student_Test()- 此测试更新- student在- course中的- score;
- public void Drop_And_Add_Student_To_Course_Test()- 此测试同时从- class中删除- student并注册到另一个- class。
此单元测试类还表明,查询数据库以加载 student 注册的所有 course 非常容易。
运行单元测试
要运行单元测试,您需要运行 MySQL 服务器。您可以通过以下命令在 Linux 系统中启动 MySQL 服务器
sudo service mysql start
然后,您可以通过 Maven 命令运行单元测试。
mvn test
如果您将项目加载到 Eclipse 中,您也可以通过 TestNG 插件运行单元测试。

关注点
- 这是关于 Hibernate 多对多映射的笔记。
- 希望您喜欢我的帖子,也希望这篇笔记能以某种方式帮助到您。
历史
- 2020 年 1 月 3 日:首次修订



