本文介绍 Spring Boot JPA @OneToMany
和 @ManyToOne
双向映射的使用方法。
目录
- 开发环境
- 基础示例
- 总结
开发环境
- JDK 8
- MySQL 8
基础示例
- 创建数据表。
CREATE SCHEMA `test` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
USE `test`;
CREATE TABLE `student` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`number` CHAR(10) NOT NULL COMMENT '学号',
`name` VARCHAR(30) NOT NULL COMMENT '姓名',
PRIMARY KEY (`id`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC) VISIBLE,
UNIQUE INDEX `number_UNIQUE` (`number` ASC) VISIBLE)
COMMENT = '学生表';
CREATE TABLE `class` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` VARCHAR(30) NOT NULL COMMENT '班级名称',
PRIMARY KEY (`id`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC) VISIBLE)
COMMENT = '班级表';
CREATE TABLE `class_t_student` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`class_id` BIGINT NOT NULL COMMENT '班级主键ID',
`student_id` BIGINT NOT NULL COMMENT '学生主键ID',
PRIMARY KEY (`id`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC) VISIBLE,
UNIQUE INDEX `class_student_UNIQUE` (`class_id` ASC, `student_id` ASC) VISIBLE)
COMMENT = '班级学生关联关系表';
如何创建 Spring Boot JPA 工程请参考:https://www.jianshu.com/p/e2b64d5c6107
创建 PO(Persistence Object) 对象。
package tutorial.spring.boot.domain;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "class")
public class ClassPO {
/**
* 自增主键
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 姓名
*/
private String name;
/**
* 学生
*/
@OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.REMOVE})
@JoinTable(name = "class_t_student",
joinColumns = @JoinColumn(name = "class_id"),
inverseJoinColumns = @JoinColumn(name = "student_id"))
private List students;
// Getter、Setter 和 toString 方法略
}
package tutorial.spring.boot.domain;
import javax.persistence.*;
@Entity
@Table(name = "student")
public class StudentPO {
/**
* 自增主键
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 学号
*/
private String number;
/**
* 姓名
*/
private String name;
/**
* 所属班级
*/
@ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE})
@JoinTable(name = "class_t_student",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "class_id"))
private ClassPO theClass;
// Getter、Setter 和 toString 方法略
}
- 创建继承 JpaRepository 的 Repository 接口类。
package tutorial.spring.boot.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import tutorial.spring.boot.domain.ClassPO;
@Repository
public interface ClassRepository extends JpaRepository {
}
package tutorial.spring.boot.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import tutorial.spring.boot.domain.StudentPO;
@Repository
public interface StudentRepository extends JpaRepository {
}
- 编写单元测试。
package tutorial.spring.boot.dao;
import org.apache.commons.lang3.RandomStringUtils;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import tutorial.spring.boot.domain.ClassPO;
import tutorial.spring.boot.domain.StudentPO;
import java.util.Collections;
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class RepositoryTest {
@Autowired
private ClassRepository classRepository;
@Autowired
private StudentRepository studentRepository;
@Test
@Order(1)
void testInsertStudent() {
// class 和 student 表中都没有记录
Assertions.assertThat(classRepository.count()).isEqualTo(0);
Assertions.assertThat(studentRepository.count()).isEqualTo(0);
// 创建 class 和 student 实体对象并将其绑定起来
ClassPO theClass = new ClassPO();
theClass.setName(RandomStringUtils.randomAlphanumeric(1, 10));
StudentPO student = new StudentPO();
student.setNumber(RandomStringUtils.randomAlphanumeric(10));
student.setName(RandomStringUtils.randomAlphanumeric(2, 20));
student.setTheClass(theClass);
// 因为 student 级联类型为 CascadeType.MERGE,所以级联新增会产生异常
Assertions.assertThatThrownBy(() -> studentRepository.save(student))
.isInstanceOf(InvalidDataAccessApiUsageException.class);
// 去除关联关系,只保存 student 成功
student.setTheClass(null);
studentRepository.save(student);
}
@Test
@Order(2)
void testInsertClass() {
// 依赖于之前的单元测试,此时 student 表中有一条记录,class 表中无记录
Assertions.assertThat(classRepository.count()).isEqualTo(0);
Assertions.assertThat(studentRepository.count()).isEqualTo(1);
// 创建 class 和 student 实体对象并将其绑定起来
ClassPO theClass = new ClassPO();
theClass.setName(RandomStringUtils.randomAlphanumeric(1, 10));
StudentPO student = new StudentPO();
student.setNumber(RandomStringUtils.randomAlphanumeric(10));
student.setName(RandomStringUtils.randomAlphanumeric(2, 20));
theClass.setStudents(Collections.singletonList(student));
// 因为 class 级联类型为 CascadeType.REMOVE,所以级联新增会产生异常
Assertions.assertThatThrownBy(() -> classRepository.save(theClass))
.isInstanceOf(InvalidDataAccessApiUsageException.class);
// 去除关联关系,只保存 class 成功
theClass.setStudents(null);
classRepository.save(theClass);
}
@Test
@Order(3)
void testUpdateStudent() {
// 依赖于之前的单元测试,此时 class 和 student 表中应各有一条记录
Assertions.assertThat(classRepository.count()).isEqualTo(1);
Assertions.assertThat(studentRepository.count()).isEqualTo(1);
ClassPO theClass = classRepository.findAll().get(0);
StudentPO student = studentRepository.findAll().get(0);
// 绑定并保存关联关系
student.setTheClass(theClass);
studentRepository.save(student);
// 更新 class 数据
String originalClassName = theClass.getName();
Assertions.assertThat(student.getTheClass().getName()).isEqualTo(originalClassName);
// 通过保存 student 级联更新 class,因为 student 级联类型为 CascadeType.MERGE,所以级联更新成功
student.getTheClass().setName(originalClassName + RandomStringUtils.randomAlphabetic(1));
studentRepository.save(student);
Assertions.assertThat(studentRepository.count()).isEqualTo(1);
StudentPO findStudent = studentRepository.findAll().get(0);
Assertions.assertThat(classRepository.count()).isEqualTo(1);
ClassPO findClass = classRepository.findAll().get(0);
Assertions.assertThat(findStudent.getTheClass().getName())
.isEqualTo(findClass.getName())
.isNotEqualTo(originalClassName);
}
@Test
@Order(4)
void testUpdateClass() {
// 依赖于之前的单元测试,此时 class 和 student 表中应各有一条记录
Assertions.assertThat(classRepository.count()).isEqualTo(1);
Assertions.assertThat(studentRepository.count()).isEqualTo(1);
ClassPO theClass = classRepository.findAll().get(0);
StudentPO student = studentRepository.findAll().get(0);
// 更新 student 数据
String studentName = student.getName();
Assertions.assertThat(theClass.getStudents().get(0).getName()).isEqualTo(studentName);
// 通过保存 class 级联更新 student,因为 class 级联类型为 CascadeType.REMOVE,所以级联更新失败,student 数据不会改变
theClass.getStudents().get(0).setName(studentName + RandomStringUtils.randomAlphabetic(1));
classRepository.save(theClass);
Assertions.assertThat(studentRepository.count()).isEqualTo(1);
StudentPO findStudent = studentRepository.findAll().get(0);
Assertions.assertThat(classRepository.count()).isEqualTo(1);
Assertions.assertThat(findStudent.getName()).isEqualTo(studentName);
}
@Test
@Order(5)
void testDelete() {
// 依赖于之前的单元测试,此时 class 和 student 表中应各有一条记录
Assertions.assertThat(classRepository.count()).isEqualTo(1);
Assertions.assertThat(studentRepository.count()).isEqualTo(1);
// 执行删除 student 操作,因为 student 级联类型为 CascadeType.MERGE,所以只会删除自己和关联表记录,无法删除 class
StudentPO student = studentRepository.findAll().get(0);
studentRepository.delete(student);
Assertions.assertThat(classRepository.count()).isEqualTo(1);
Assertions.assertThat(studentRepository.count()).isEqualTo(0);
/*
* 执行删除 class 操作,因为 class 级联类型为 CascadeType.REMOVE,所以会删除自己及关联的 student 记录
* 为了验证级联删除效果,需要将屏蔽删除 student 操作的代码段
*/
ClassPO theClass = classRepository.findAll().get(0);
classRepository.delete(theClass);
Assertions.assertThat(classRepository.count()).isEqualTo(0);
Assertions.assertThat(studentRepository.count()).isEqualTo(0);
}
}
总结
- 本示例中建立了双向映射,单向映射可以参考:Spring Boot JPA @ManyToOne 单向映射;
- 本实例中使用了【表关联】的表结构设计策略,针对一对多和多对一的实体关系而言,还可以使用【外键关联】的表结构设计策略。