[spring] Spring JPA - Hibernate 多表联查 2

[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,即删除老师不需要联动删除课程,反之亦然

具体图如下:

instructor
course1
course2
course3

⚠️:这会是一个 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

    一般来说这种实现更好,不过它也会存在两个问题:

    1. 需要一个打开的 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;
      

      那么数据就会正常渲染:

      在这里插入图片描述

    2. @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 教的所有课,案例如下:

  1. 新增加一个方法获取 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();
    }
    
  2. 使用新的方法

    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");
    }
    
  3. 结果:

    在这里插入图片描述

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);
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值