[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;
}
@Override
public 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;
}
@Override
public String toString() {
return "Instructor{" +
"instructorDetail=" + instructorDetail +
", email='" + email + '\'' +
", lastname='" + lastname + '\'' +
", firstname='" + firstname + '\'' +
", id=" + id +
'}';
}
// add convenience methods for bi-directional relationship
public 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 instructor
Instructor 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 objects
instructor.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 instructor System.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;
} @Override
public 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
@Transactional
public 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);
@Override
public Course findCourseById(int id) {
return entityManager.find(Course.class, id);
}
@Override
@Transactional
public 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
@Transactional
public 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;
}
@Override
public 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
@Transactional
public 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 去做搜索
@Override
public 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);
}