本文介紹 Spring Boot JPA @OneToMany
和 @ManyToOne
雙向映射的使用方法捶朵。
目錄
- 開發(fā)環(huán)境
- 基礎(chǔ)示例
- 總結(jié)
開發(fā)環(huán)境
- JDK 8
- MySQL 8
基礎(chǔ)示例
- 創(chuàng)建數(shù)據(jù)表。
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 '學(xué)號',
`name` VARCHAR(30) NOT NULL COMMENT '姓名',
PRIMARY KEY (`id`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC) VISIBLE,
UNIQUE INDEX `number_UNIQUE` (`number` ASC) VISIBLE)
COMMENT = '學(xué)生表';
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 '學(xué)生主鍵ID',
PRIMARY KEY (`id`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC) VISIBLE,
UNIQUE INDEX `class_student_UNIQUE` (`class_id` ASC, `student_id` ASC) VISIBLE)
COMMENT = '班級學(xué)生關(guān)聯(lián)關(guān)系表';
如何創(chuàng)建 Spring Boot JPA 工程請參考:http://www.reibang.com/p/e2b64d5c6107
創(chuàng)建 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;
/**
* 學(xué)生
*/
@OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.REMOVE})
@JoinTable(name = "class_t_student",
joinColumns = @JoinColumn(name = "class_id"),
inverseJoinColumns = @JoinColumn(name = "student_id"))
private List<StudentPO> 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;
/**
* 學(xué)號
*/
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 方法略
}
- 創(chuàng)建繼承 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<ClassPO, Long> {
}
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<StudentPO, Long> {
}
- 編寫單元測試鞭呕。
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);
// 創(chuàng)建 class 和 student 實(shí)體對象并將其綁定起來
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);
// 因?yàn)?student 級聯(lián)類型為 CascadeType.MERGE,所以級聯(lián)新增會產(chǎn)生異常
Assertions.assertThatThrownBy(() -> studentRepository.save(student))
.isInstanceOf(InvalidDataAccessApiUsageException.class);
// 去除關(guān)聯(lián)關(guān)系篇恒,只保存 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);
// 創(chuàng)建 class 和 student 實(shí)體對象并將其綁定起來
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));
// 因?yàn)?class 級聯(lián)類型為 CascadeType.REMOVE婚度,所以級聯(lián)新增會產(chǎn)生異常
Assertions.assertThatThrownBy(() -> classRepository.save(theClass))
.isInstanceOf(InvalidDataAccessApiUsageException.class);
// 去除關(guān)聯(lián)關(guān)系蘸秘,只保存 class 成功
theClass.setStudents(null);
classRepository.save(theClass);
}
@Test
@Order(3)
void testUpdateStudent() {
// 依賴于之前的單元測試,此時 class 和 student 表中應(yīng)各有一條記錄
Assertions.assertThat(classRepository.count()).isEqualTo(1);
Assertions.assertThat(studentRepository.count()).isEqualTo(1);
ClassPO theClass = classRepository.findAll().get(0);
StudentPO student = studentRepository.findAll().get(0);
// 綁定并保存關(guān)聯(lián)關(guān)系
student.setTheClass(theClass);
studentRepository.save(student);
// 更新 class 數(shù)據(jù)
String originalClassName = theClass.getName();
Assertions.assertThat(student.getTheClass().getName()).isEqualTo(originalClassName);
// 通過保存 student 級聯(lián)更新 class蝗茁,因?yàn)?student 級聯(lián)類型為 CascadeType.MERGE醋虏,所以級聯(lián)更新成功
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 表中應(yīng)各有一條記錄
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 數(shù)據(jù)
String studentName = student.getName();
Assertions.assertThat(theClass.getStudents().get(0).getName()).isEqualTo(studentName);
// 通過保存 class 級聯(lián)更新 student哮翘,因?yàn)?class 級聯(lián)類型為 CascadeType.REMOVE颈嚼,所以級聯(lián)更新失敗,student 數(shù)據(jù)不會改變
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 表中應(yīng)各有一條記錄
Assertions.assertThat(classRepository.count()).isEqualTo(1);
Assertions.assertThat(studentRepository.count()).isEqualTo(1);
// 執(zhí)行刪除 student 操作阻课,因?yàn)?student 級聯(lián)類型為 CascadeType.MERGE叫挟,所以只會刪除自己和關(guān)聯(lián)表記錄,無法刪除 class
StudentPO student = studentRepository.findAll().get(0);
studentRepository.delete(student);
Assertions.assertThat(classRepository.count()).isEqualTo(1);
Assertions.assertThat(studentRepository.count()).isEqualTo(0);
/*
* 執(zhí)行刪除 class 操作限煞,因?yàn)?class 級聯(lián)類型為 CascadeType.REMOVE抹恳,所以會刪除自己及關(guān)聯(lián)的 student 記錄
* 為了驗(yàn)證級聯(lián)刪除效果,需要將屏蔽刪除 student 操作的代碼段
*/
ClassPO theClass = classRepository.findAll().get(0);
classRepository.delete(theClass);
Assertions.assertThat(classRepository.count()).isEqualTo(0);
Assertions.assertThat(studentRepository.count()).isEqualTo(0);
}
}
總結(jié)
- 本示例中建立了雙向映射署驻,單向映射可以參考:Spring Boot JPA @ManyToOne 單向映射奋献;
- 本實(shí)例中使用了【表關(guān)聯(lián)】的表結(jié)構(gòu)設(shè)計策略,針對一對多和多對一的實(shí)體關(guān)系而言旺上,還可以使用【外鍵關(guān)聯(lián)】的表結(jié)構(gòu)設(shè)計策略瓶蚂。