在Hibernate的ORM映射场景中,多对多关系是非常常见的关联类型,默认情况下Hibernate会自动生成一个仅包含双方主键的中间表来存储关联关系,但这种默认方式无法满足中间表需要存储额外业务属性的需求,还会造成不必要的表结构冗余。通过自定义中间实体,我们可以将多对多关系拆解为两个一对多关系,从而灵活控制中间表的结构和字段内容。

多对多默认映射的问题
假设我们有两个实体Student和Course,一个学生可以选多门课程,一门课程可以被多个学生选择,默认的多对多映射会生成一个student_course中间表,仅包含student_id和course_id两个字段。如果我们需要记录学生选课的时间、考试成绩等额外信息,默认的中间表就无法满足需求,此时就需要自定义中间实体。
自定义中间实体的实现思路
自定义中间实体的核心是将多对多关系拆分为两个一对多关系:学生实体与中间实体是一对多,课程实体与中间实体也是一对多。中间实体中除了存储双方的主键作为外键,还可以添加业务需要的额外字段。
1. 实体类设计
首先定义Student实体和Course实体,然后定义中间实体StudentCourse。
// Student实体类
@Entity
@Table(name = "t_student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 与中间实体的一对多关系,mappedBy指向中间实体中的student属性
@OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)
private List<StudentCourse> studentCourseList = new ArrayList<>();
// 省略getter、setter方法
}
// Course实体类
@Entity
@Table(name = "t_course")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String courseName;
// 与中间实体的一对多关系,mappedBy指向中间实体中的course属性
@OneToMany(mappedBy = "course", cascade = CascadeType.ALL, orphanRemoval = true)
private List<StudentCourse> studentCourseList = new ArrayList<>();
// 省略getter、setter方法
}
// 自定义中间实体StudentCourse
@Entity
@Table(name = "t_student_course")
public class StudentCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 选课时间
private Date selectTime;
// 考试成绩
private Integer score;
// 与Student的一对多关系,多端维护外键
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
// 与Course的一对多关系,多端维护外键
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
// 省略getter、setter方法
}
2. 映射关系说明
上述设计中,StudentCourse作为中间实体,同时关联Student和Course,通过@ManyToOne注解维护两个外键关系,同时可以自定义selectTime、score等业务字段。此时生成的t_student_course表会包含id、student_id、course_id、select_time、score等字段,完全符合业务需求,不会生成冗余的默认中间表。
操作示例
下面是添加学生和课程关联关系的代码示例:
public class Test {
public static void main(String[] args) {
// 假设已经获取Session实例
Session session = HibernateUtil.getSession();
Transaction transaction = session.beginTransaction();
// 创建学生
Student student = new Student();
student.setName("张三");
session.save(student);
// 创建课程
Course course = new Course();
course.setCourseName("Java编程");
session.save(course);
// 创建中间实体,设置关联关系和业务属性
StudentCourse studentCourse = new StudentCourse();
studentCourse.setStudent(student);
studentCourse.setCourse(course);
studentCourse.setSelectTime(new Date());
studentCourse.setScore(90);
// 保存中间实体
session.save(studentCourse);
transaction.commit();
session.close();
}
}
注意事项
- 中间实体的
@ManyToOne注解需要配置@JoinColumn指定外键字段名,避免生成默认的外键名称。 - 如果需要在
Student或Course中直接获取对方的集合,可以在实体中添加辅助方法,例如Student中添加一个获取所有选课课程的方法:
// Student实体中添加的方法
public List<Course> getCourseList() {
List<Course> courseList = new ArrayList<>();
for (StudentCourse sc : studentCourseList) {
courseList.add(sc.getCourse());
}
return courseList;
}
这种方式既保留了多对多关系的查询便利性,又避免了默认映射带来的冗余表问题,让数据库表结构更贴合实际业务场景。