[spring] Spring JPA - Hibernate 多表联查 2
这篇笔记基于 [spring] Spring JPA - Hibernate 多表联查 1 之上进行的实现,主要的 skeleton 都在那里了,代码不一定会全部 cv
这篇主要实现的是 one-to-many 和 many-to-one 的关系。这个实现起来和上一篇里用 @OneToOne 以及 @OneToOne(mappedBy) 的方向是一致的,同样是一个表里设有 foreign key(一半在 many 的那个实例中),另一个没有 foreign key 的通过 mappedBy 去做 join 搜索
one to many & many to one
这次新增的实例为 course,这里的逻辑一个老师可以很多课——这里不考虑同一个课会被多个老师教,即不同 section 的条件
⚠️:不需要使用 cascading delete,即删除老师不需要联动删除课程,反之亦然
具体图如下:
⚠️:这会是一个 bi-directional 的关系
更新数据库
这里也同样提供 sql:
DROP SCHEMA IF EXISTS `hb-03-one-to-many`;CREATE SCHEMA `hb-03-one-to-many`;use `hb-03-one-to-many`;SET FOREIGN_KEY_CHECKS = 0;DROP TABLE IF EXISTS `instructor_detail`;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;DROP TABLE IF EXISTS `instructor`;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;DROP TABLE IF EXISTS `course`;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;SET FOREIGN_KEY_CHECKS = 1;
运行后的结果应该如下:

⚠️:如果 spring boot 遇到了连接问题,可以查看 [spring] Spring JPA - Hibernate 多表联查 1 中的 reference 部分解决
代码设置
实现 Course Entity
package com.example.demo.entity;import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.hibernate.annotations.Cascade;@Data
@NoArgsConstructor
@Entity
@Table(name = "course")
public class Course {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)@Column(name = "id")private int id;@Column(name = "title")private String title;@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.DETACH,CascadeType.MERGE, CascadeType.REFRESH })@JoinColumn(name = "instructor_id")private Instructor instructor;public Course(String title) {this.title = title;}@Overridepublic String toString() {return "Course{" +"id=" + id +", title='" + title + '\'' +'}';}
}
⚠️:instructor-to-course 是 one-to-many 的关系,正常情况下是将 foreign key 放到 many 的这个部分——这个案例中也就是 course 里,所以这里才会用 @JoinColumn(name = "instructor_id")
更新 instructor entity
package com.example.demo.entity;import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.ArrayList;
import java.util.List;@Entity
@Table(name = "instructor")
@Data
@NoArgsConstructor
public class Instructor {@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;// set up mapping to InstructDetail@OneToOne(cascade = CascadeType.ALL)@JoinColumn(name = "instructor_detail_id")private InstructorDetail instructorDetail;@OneToMany(mappedBy = "instructor",cascade = { CascadeType.PERSIST, CascadeType.DETACH,CascadeType.MERGE, CascadeType.REFRESH })private List<Course> courses;public Instructor(String firstname, String lastname, String email) {this.firstname = firstname;this.lastname = lastname;this.email = email;}@Overridepublic String toString() {return "Instructor{" +"instructorDetail=" + instructorDetail +", email='" + email + '\'' +", lastname='" + lastname + '\'' +", firstname='" + firstname + '\'' +", id=" + id +'}';}// add convenience methods for bi-directional relationshippublic void add(Course course) {if (courses == null) {courses = new ArrayList<>();}courses.add(course);course.setInstructor(this);}
}
⚠️:这里主要更新的部分是 private List<Course> courses; 以及其对应的注解,在上面的图解中已经表明了,一个老师可以上好几门课,所以这里需要用一个 List 去存储对应的数据
👀:这里新增了一个 util 方法,即 public void add(Course course) ,主要是可以方便的添加课程
update properties file
主要是需要更新一下数据库相关的部分,也就是 datasource.url
spring.datasource.url=jdbc:mysql://localhost:3306/hb-01-one-to-one-uni
添加有课的老师
这里主要更新的是 cli 部分的代码:
private void createInstructorWithCourses(AppDAO appDAO) {// create the instructorInstructor instructor = new Instructor("Peter", "Parker", "peter.p@gmail.com");InstructorDetail instructorDetail = new InstructorDetail("http://www.example.com", "Piano");Course course1 = new Course("Guitar");Course course2 = new Course("Paint ball");// associate the objectsinstructor.setInstructorDetail(instructorDetail);instructor.add(course1);instructor.add(course2);System.out.println("Saving instructor: " + instructor);System.out.println("Courses: " + instructor.getCourses());appDAO.save(instructor);System.out.println("Done!");}
效果如下:


fetch 类型
上篇有简单的题过 fetch 的类型分为 eager 和 lazy 两种,二者的区别如下:
-
eager 会获取所有的关联实例
以当前案例来说,在获取 instructor 的数据后,它同时会获取所有关联的 courses
当存在多个 instructors 的情况下,这就类似于执行了下面这个 query:
SELECT * FROM instructor;SELECT * FROM course WHERE instructor_id = 1; SELECT * FROM course WHERE instructor_id = 2; SELECT * FROM course WHERE instructor_id = 3;这也就遇到了比较经典的 N+1 query 问题——即获取 1 次 instructors 数据,同时调用 instructors 长度(N)个 query 去获取关联的数据
💡:这种情况下,要做优化的话可以通过
@Query("SELECT i FROM Instructor i JOIN FETCH i.courses")去获取整个 instructor list,或者通过@EntityGraph去进行优化 -
lazy 默认情况下不会获取相关联的数据,只有在调用/访问对应的属性/getter 时,才会去重新通过 query 获取对应的数据
⚠️:
@OneToMany的默认FetchType是 lazy一般来说这种实现更好,不过它也会存在两个问题:
-
需要一个打开的 hibernate session
这个情况下来说,发生在当前方法已经返回了 instructor,然后 hibernate session 关闭。其他的方法——在非
@Transactional的方法中去访问 instructor 中的 courses,就会抛出异常触发方式如下:
更新 main 中的代码:
private void findInstructorWithCourses(AppDAO appDAO) {int id = 1;System.out.println("Finding instructor id: " + id);Instructor instructor = appDAO.findInstructorById(id);System.out.println("instructor: " + instructor);System.out.println("associated courses:" + instructor.getCourses()); }这是在 main 方法内调用的,hibernate session 已经关闭。默认情况下是进行 lazy fetch,所以 instructor 相关联的 course 也不会被获取
这个时候就会触发报错:

这个时候只要将 FetchType 改成 eager:
@OneToMany(mappedBy = "instructor",fetch = FetchType.EAGER,cascade = { CascadeType.PERSIST, CascadeType.DETACH,CascadeType.MERGE, CascadeType.REFRESH }) private List<Course> courses;那么数据就会正常渲染:

-
在
@Transactional的方法中去循环访问每个 instructor 的 courses这个依旧是会造成 n+1 query 的问题
一般来说,常见的处理方式是在 entity 中,使用 Lazy 注解,这样调用
findAll()只会跑一条 query;同时也可以新增加一个方法,通过上面的@Query去实现List<Instructor> findAllWithCourses();这样的方法,也能够有效地减缓数据库的调用 -
Lazy Fetch 的使用案例
这里会将 instructor 中的 course 获取方式设置成 lazy——即默认情况
@OneToMany(mappedBy = "instructor",fetch = FetchType.LAZY,cascade = { CascadeType.PERSIST, CascadeType.DETACH,CascadeType.MERGE, CascadeType.REFRESH })
private List<Course> courses;
直接通过 foreign key 获取
首先,因为 foreign key 设置在 course 上,所以可以直接通过 FK 去寻找一个 instructor 教的所有课,案例如下:
-
新增加一个方法获取 instructor
public interface AppDAO {List<Course> findCoursesByInstructorId(int id); }@Override public List<Course> findCoursesByInstructorId(int id) {TypedQuery<Course> query = entityManager.createQuery("from Course where instructor.id = :data", Course.class);query.setParameter("data", id);return query.getResultList(); } -
使用新的方法
private void findCoursesForInstructor(AppDAO appDAO) {int id = 1;System.out.println("finding instructor id: " + id);Instructor instructor = appDAO.findInstructorById(id);System.out.println("instructor: " + instructor);// retrieve courses for the instructorSystem.out.println("finding courses for instructor id: " + id);List<Course> courses = appDAO.findCoursesByInstructorId(id);instructor.setCourses(courses);System.out.println("associated courses: " + instructor.getCourses());System.out.println("Done"); } -
结果:

join fetch
也就是上面提到的常见解决方法
这里新增加一个方法获取 instructor
public Instructor findInstructorByIdJoinFetch(int id) {return null;} @Overridepublic Instructor findInstructorByIdJoinFetch(int id) {TypedQuery<Instructor> query = entityManager.createQuery("select i from Instructor i "+ "JOIN FETCH i.courses "+ "where i.id = :data", Instructor.class);query.setParameter("data", id);return query.getSingleResult();}
private void findInstructorWithCoursesJoinFetch(AppDAO appDAO) {int id = 1;System.out.println("finding instructor id: " + id);Instructor instructor = appDAO.findInstructorByIdJoinFetch(id);System.out.println(instructor);System.out.println("associated courses: " + instructor.getCourses());System.out.println("Done");}
调用结果如下:

更新实例(Update 操作)
更新 instructor
这里通过 merge 实现,代码修改如下:
void update(Instructor instructor);
@Override@Transactionalpublic void update(Instructor instructor) {entityManager.merge(instructor);}
private void updateInstructor(AppDAO appDAO) {int id = 1;System.out.println("Finding instructor id: " + id);Instructor instructor = appDAO.findInstructorByIdJoinFetch(id);System.out.println("Updating instructor id: " + id);instructor.setLastname("TESTER");appDAO.update(instructor);System.out.println("Done");}
效果:

| 前 | 后 |
|---|---|
![]() | ![]() |
更新 course
核心逻辑是一样的
Course findCourseById(int id);void update(Course course);
@Overridepublic Course findCourseById(int id) {return entityManager.find(Course.class, id);}@Override@Transactionalpublic void update(Course course) {entityManager.merge(course);}
private void updateCourse(AppDAO appDAO) {int id = 10;System.out.println("Finding course id: " + id);Course course = appDAO.findCourseById(id);System.out.println("Updating course id: " + id);course.setTitle("New Course");appDAO.update(course);}

前后对比:
| 前 | 后 |
|---|---|
![]() | ![]() |
复习一下 merge 的操作:
会将 detached entity 的变更数据 复制到当前 管理中的 entity。如果数据库中已有该 entity,则会 更新 现有记录;如果没有,则会 插入 新记录
更新删除实例
这里主要的操作就是将所有的 course 删除掉,否则会有 foreign key constraint 的问题
@Override@Transactionalpublic void deleteInstructorById(int id) {Instructor instructor = this.findInstructorById(id);if (instructor != null) {List<Course> courses = instructor.getCourses();for (Course course : courses) {course.setInstructor(null);}entityManager.remove(instructor);}}
这里还是 n+1 query 的操作,可以通过手写一些 query 去提升效果
新增 uni-directional 的 Review 实例
这个基本上就是把东西重复一下,练一下手
主要 course --> review 是一个单方面的,one-to-many 的关系。当然,具体实现也是根据之前说的,在 many 的实例上设置 foreign key
数据库修改
sql 脚本如下:
DROP SCHEMA IF EXISTS `hb-04-one-to-many-uni`;CREATE SCHEMA `hb-04-one-to-many-uni`;use `hb-04-one-to-many-uni`;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;SET FOREIGN_KEY_CHECKS = 1;

实现 review 实例
代码基本上一致,
package com.example.demo.entity;import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@Entity
@Table(name = "review")
public class Review {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)@Column(name = "id")private int id;@Column(name = "comment")private String comment;public Review(String comment) {this.comment = comment;}@Overridepublic String toString() {return "Review{" +"id=" + id +", comment='" + comment + '\'' +'}';}
}
⚠️:这里 review 并没有和 course 建立任何关系,所以是 course --> review 的单方面联系
重构 course
如上面提到的,新增 one-to-many 的关系
public class Course {//...@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)@JoinColumn(name = "course_id")private List<Review> reviews;//...public void addReview(Review review) {if (reviews == null) {reviews = new ArrayList<>();}reviews.add(review);}
}
更新 DAO 和 main
这里就基本还是那几个变化了:
@Override@Transactionalpublic void save(Course course) {entityManager.persist(course);}
private void createCourseAndReviews(AppDAO appDAO) {Course course = new Course("Minesweeper");course.addReview(new Review("Finish advanced level in 100s!"));course.addReview(new Review("Great course!"));course.addReview(new Review("Love it!"));course.addReview(new Review("Average..."));System.out.println("Saving the course...");System.out.println(course);System.out.println(course.getReviews());appDAO.save(course);}
结果如下:

获取 course 和 review
这里也使用 join 去做搜索
@Overridepublic Course findCourseAndReviewsByCourseId(int id) {TypedQuery<Course> query = entityManager.createQuery("select c from Course c "+ "JOIN FETCH c.reviews "+ "where c.id = :data",Course.class);query.setParameter("data", id);return query.getSingleResult();}
删除 course 和 review
这个实现更简单了,因为有 cascading delete,所以直接删即可
private void deleteCourseAndReviews(AppDAO appDAO) {int id = 10;System.out.println("Deleting course id: " + id);appDAO.deleteCourseById(id);}




