[spring] Spring JPA - Hibernate 多表联查 3
[spring] Spring JPA - Hibernate 多表联查 3
这篇笔记基于:
- [spring] Spring JPA - Hibernate 多表联查 1
- [spring] Spring JPA - Hibernate 多表联查 2
进行实现
这是目前对于 Hibernate 学习的最后一步了,主要学习一下 many-to-many 的关系
之前的内容都覆盖的差不多了,整体来说这篇笔记会比较短,也没这么多代码
Many to Many
many-to-many 应该是日常生活中遇到的最多的情况,如 course <-> student,但是在这种情况下,如果需要使用外链去存储对应的关系,那么就会让数据变得非常难以管理,并且搜索的效率也会非常的低下。
目前比较常见的一个处理方法是使用 join table,去单独存储二者之间的 mapping
更新数据库
DROP SCHEMA IF EXISTS `hb-05-many-to-many`;
CREATE SCHEMA `hb-05-many-to-many`;
use `hb-05-many-to-many`;
SET FOREIGN_KEY_CHECKS = 0;
CREATE TABLE `instructor_detail` (
`id` int NOT NULL AUTO_INCREMENT,
`youtube_channel` varchar(128) DEFAULT NULL,
`hobby` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
CREATE TABLE `instructor` (
`id` int NOT NULL AUTO_INCREMENT,
`first_name` varchar(45) DEFAULT NULL,
`last_name` varchar(45) DEFAULT NULL,
`email` varchar(45) DEFAULT NULL,
`instructor_detail_id` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_DETAIL_idx` (`instructor_detail_id`),
CONSTRAINT `FK_DETAIL` FOREIGN KEY (`instructor_detail_id`)
REFERENCES `instructor_detail` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
CREATE TABLE `course` (
`id` int NOT NULL AUTO_INCREMENT,
`title` varchar(128) DEFAULT NULL,
`instructor_id` int DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `TITLE_UNIQUE` (`title`),
KEY `FK_INSTRUCTOR_idx` (`instructor_id`),
CONSTRAINT `FK_INSTRUCTOR`
FOREIGN KEY (`instructor_id`)
REFERENCES `instructor` (`id`)
ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=latin1;
CREATE TABLE `review` (
`id` int NOT NULL AUTO_INCREMENT,
`comment` varchar(256) DEFAULT NULL,
`course_id` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_COURSE_ID_idx` (`course_id`),
CONSTRAINT `FK_COURSE`
FOREIGN KEY (`course_id`)
REFERENCES `course` (`id`)
ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
CREATE TABLE `student` (
`id` int 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 NOT NULL,
`student_id` int 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;
SET FOREIGN_KEY_CHECKS = 1;
跑完后的结果应该是这样的:
下面一排和之前的案例是一样的,这里就是新增了两张表格
实现学生实例 & 更新课程实例
简单的代码,写了蛮多遍了:
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "student")
@Data
@NoArgsConstructor
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;
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 + '\'' +
'}';
}
}
下面是实现 many-to-many 的部分
更新课程 entity 获取学生
@JoinTable(
name = "course_student",
joinColumns = @JoinColumn(name = "course_id"),
inverseJoinColumns = @JoinColumn(name = "student_id")
)
private List<Student> students;
这个部分理解起来还是比较简单的:
JoinTable
- 顾名思义,联表joinColumns
- 指的是当前 entity 在表中对应的 idinverseJoinColumns
- 目标 entity 的 id
更新学生 entity 获取课程
@ManyToMany(fetch = FetchType.LAZY,
cascade = { CascadeType.PERSIST, CascadeType.DETACH,
CascadeType.MERGE, CascadeType.REFRESH })
@JoinTable(
name = "course_student",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses;
student 和 course 的实现基本是一致的,但是因为 entity 的不同,所以 joinColumns
和 inverseJoinColumns
所 mapping 的值需要进行对调
更新 main
private void createCourseAndStudents(AppDAO appDAO) {
Course course = new Course("Table Tennis");
Student student1 = new Student("John", "Doe", "john_doe@gmail.com");
Student student2 = new Student("Mary", "Jane", "m_jane@gmail.com");
course.addStudent(student1);
course.addStudent(student2);
System.out.println("Saving the course: " + course);
System.out.println("associated students: " + course.getStudents());
appDAO.save(course);
}
这个写的应该也是蛮多了,没什么特别大的差异,主要就是跑一下证明一下代码没写错
效果如下:
寻找上 A 课的学生
这里最关键的是 courseId
,找的是所有注册 :course_id
的课
DAOImpl 的实现:
@Override
public Course findCourseAndStudentByCourseId(int id) {
TypedQuery<Course> query = entityManager.createQuery(
"select c from Course c "
+ "JOIN FETCH c.students "
+ "where c.id = :data",
Course.class
);
query.setParameter("data", id);
return query.getSingleResult();
}
main 的更新:
private void findCourseAndStudents(AppDAO appDAO) {
int id = 10;
Course course = appDAO.findCourseAndStudentByCourseId(id);
System.out.println(course);
}
效果如下:
找 B 学生要上的所有课
这里最关键的是 studentId
,找的是所有注册 :student_id
的课
DAOImpl 的实现如下:
@Override
public Student findStudentAndCoursesByStudentId(int id) {
TypedQuery<Student> query = entityManager.createQuery(
"select s from Student s "
+ "JOIN FETCH s.courses "
+ "where s.id = :data",
Student.class
);
query.setParameter("data", id);
return query.getSingleResult();
}
main 的更新如下:
private void findStudentAndCourses(AppDAO appDAO) {
int id = 1;
Student student = appDAO.findStudentAndCoursesByStudentId(id);
System.out.println(student);
System.out.println(student.getCourses());
}
效果截图:
删除课程
这部分的代码其实没必要变动,一样可以跑:
主要还是因为 cascade type 里面没有什么 delete 的关系
删除学生
@Override
@Transactional
public void deleteStydentById(int id) {
Student student = entityManager.find(Student.class, id);
if (student != null) {
entityManager.remove(student);
}
}
在 remove
之前调用一下 clear
也可以手动断开所有的 foreign key 关联。一般来说 map 的比较好的,JPA 可以自动进行清理——也就是 demo app 跑的
但是如果是要用在 production 上的话,还是加一个 student.getCourses().clear();
比较好