SpringBoot学习之旅(八)---JPA进阶篇之联表操作
文章目录
- 前言
- 源码下载
- 其他文章
- 数据关联关系映射
- 数据库创建
- 数据库表映射对象
- 操作及测试
- 学生操作及测试
- 其他功能测试
- 常见问题
- 总结
前言
前两节有说道JPA的基础操作及JPA自定义查询、修改、分页等操作,有兴趣的可以去看看,前面两章的所有操作都只是在操作一张表,但是真实的项目中,不可能所有的业务都只去做单标的操作,因为这不符合数据库表的设计法则,因此,各个表之间就总会发生那么点正当、或者不正当的关系;那么我们就得去理清各个要素之间的关系;
源码下载
点击开源中国下载源码
由于代码内容比较多多,建议优先下载代码,对着代码读更加快捷
其他文章
SpringBoot学习之旅(七)—JPA进阶篇之自定义查询、修改、分页
数据关联关系映射
关系类型 | Owning-Side | Inverse-Side |
---|---|---|
one-to-one | @OneToOne | @OneToOne(mappedBy=“othersideName”) |
one-to-many / many-to-one | @ManyToOne | @OneToMany(mappedBy=“xxx”) |
many-to-many | @ManyToMany | @ManyToMany(mappedBy =“xxx”) |
针对以上关系,举一下常见例子
- 一对一
一个学生只能对应一个档案编号及学号 - 一对多/多对一
一个班级可以容纳多个学生 - 多对多
一个学生可以选择多门学科,一个学科允许有多个学生学习
数据库创建
表结构
-- 学生信息表
DROP TABLE IF EXISTS `student_info`;
CREATE TABLE `student_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '索引ID',
`stu_name` varchar(20) NOT NULL COMMENT '学生姓名',
`stu_sex` int(2) NOT NULL COMMENT '学生性别',
`stu_age` int(3) NOT NULL COMMENT '学生年龄',
`stu_class_id` int(5) NOT NULL COMMENT '学生班级ID',
`stu_file_id` int(5) NOT NULL COMMENT '学生档案ID',
PRIMARY KEY (`id`),
KEY `student_info_class_id` (`stu_class_id`),
KEY `student_info_file_id` (`stu_file_id`),
CONSTRAINT `student_info_class_id` FOREIGN KEY (`stu_class_id`) REFERENCES `class_info` (`id`),
CONSTRAINT `student_info_file_id` FOREIGN KEY (`stu_file_id`) REFERENCES `file_info` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of student_info
-- ----------------------------
INSERT INTO `student_info` VALUES ('1', '张三', '0', '7', '1', '1');
INSERT INTO `student_info` VALUES ('2', '李四', '1', '7', '1', '2');
INSERT INTO `student_info` VALUES ('3', '王五', '2', '7', '2', '3');
-- 班级表
DROP TABLE IF EXISTS `class_info`;
CREATE TABLE `class_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '班级ID',
`class_name` varchar(255) DEFAULT NULL COMMENT '班级名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of class_info
-- ----------------------------
INSERT INTO `class_info` VALUES ('1', '一年级一班');
INSERT INTO `class_info` VALUES ('2', '一年级二班');
INSERT INTO `class_info` VALUES ('3', '一年级三班');
INSERT INTO `class_info` VALUES ('4', '二年级一班');
--档案表
DROP TABLE IF EXISTS `file_info`;
CREATE TABLE `file_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '索引ID',
`file_id` int(11) DEFAULT NULL COMMENT '档案编号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of file_info
-- ----------------------------
INSERT INTO `file_info` VALUES ('1', '100001');
INSERT INTO `file_info` VALUES ('2', '100002');
INSERT INTO `file_info` VALUES ('3', '100003');
-- 科目表
DROP TABLE IF EXISTS `subject_info`;
CREATE TABLE `subject_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '索引ID',
`subject_name` varchar(50) DEFAULT NULL COMMENT '科目名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of subject_info
-- ----------------------------
INSERT INTO `subject_info` VALUES ('1', '语文');
INSERT INTO `subject_info` VALUES ('2', '数学');
INSERT INTO `subject_info` VALUES ('3', '英语');
INSERT INTO `subject_info` VALUES ('4', '体育');
INSERT INTO `subject_info` VALUES ('5', '音乐');
-- 学生与科目关联表
DROP TABLE IF EXISTS `subject_selection_info`;
CREATE TABLE `subject_selection_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '索引ID',
`student_id` int(11) NOT NULL COMMENT '学生ID',
`subject_id` int(11) NOT NULL COMMENT '科目ID',
PRIMARY KEY (`id`),
KEY `subject_selection_info_stu_id` (`student_id`),
KEY `subject_selection_info_sub_id` (`subject_id`),
CONSTRAINT `subject_selection_info_stu_id` FOREIGN KEY (`student_id`) REFERENCES `student_info` (`id`),
CONSTRAINT `subject_selection_info_sub_id` FOREIGN KEY (`subject_id`) REFERENCES `subject_info` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of subject_selection_info
-- ----------------------------
INSERT INTO `subject_selection_info` VALUES ('1', '1', '1');
INSERT INTO `subject_selection_info` VALUES ('2', '1', '2');
INSERT INTO `subject_selection_info` VALUES ('3', '1', '3');
INSERT INTO `subject_selection_info` VALUES ('4', '1', '4');
INSERT INTO `subject_selection_info` VALUES ('5', '2', '1');
INSERT INTO `subject_selection_info` VALUES ('6', '2', '2');
INSERT INTO `subject_selection_info` VALUES ('7', '2', '3');
INSERT INTO `subject_selection_info` VALUES ('8', '2', '5');
INSERT INTO `subject_selection_info` VALUES ('9', '3', '1');
INSERT INTO `subject_selection_info` VALUES ('10', '3', '2');
INSERT INTO `subject_selection_info` VALUES ('11', '3', '3');
- 数据库ER图表
数据库表映射对象
本文的所有对象均使用Lombok生成对象get set方法
学生对象
package com.lupf.springbootjpa.dbobject;
import lombok.Data;
import lombok.ToString;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
@Data
@Entity
public class StudentInfo implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String stuName;
private Integer stuSex;
private Integer stuAge;
//---------以下配置是为了方便查询学生信息的时候一并查询出其关联的信息
//由于学号及档案信息是一对一的关系,因此这里是一对一的
@OneToOne(cascade = CascadeType.ALL)
//name为当前表中列的名称 referencedColumnName标表示外键关联了对方的表内容
@JoinColumn(name = "stu_file_id", referencedColumnName = "id")
private FileInfo fileInfo;
//对于学生来说,是多个学生对应一个班级,因此这里是一对多的关系
//设置为CascadeType.MERGE时,当添加数据的时候,会自动插入class表中的数据
@ManyToOne(cascade = CascadeType.MERGE)
//name在表中的列
@JoinColumn(name = "stu_class_id", referencedColumnName = "id")
private ClassInfo classInfo;
//这里的fetch要使用EAGER(全部抓取) LAZY懒加载会报错
//由于一个学生可以选择多个科目 因此这里相对与科目来说是一对多的关系
//设置为CascadeType.MERGE时,当添加数据的时候,会自动插入subject_selection_info表中的数据
@ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.EAGER)
//由于学生表和所选的科目表是使用subject_selection_info关联的 因此这里需要设置他们的关联关系
@JoinTable(name = "subject_selection_info", joinColumns = @JoinColumn(name = "student_id"), inverseJoinColumns = @JoinColumn(name = "subject_id"))
private List<SubjectInfo> subjectInfos;
}
科目对象
package com.lupf.springbootjpa.dbobject;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
@Data
@Entity
public class SubjectInfo implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String subjectName;
//这里需要加上反序列化忽略,因为StudentInfo关联了SubjectInfo 这里又关联了StudentInfo
// 就出现了你中有我,我中有你的问题,从未导致反序列话的时候堆栈溢出(死循环)的问题
@JSONField(serialize = false)
//这里关联是为了满足,删除这里面的数据的时候,自动删除subject_selection_info并在学生与科目之间的关系
@ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.EAGER)
@JoinTable(name = "subject_selection_info", joinColumns = @JoinColumn(name = "subject_id"), inverseJoinColumns = @JoinColumn(name = "student_id"))
private List<StudentInfo> studentInfos;
}
班级对象
package com.lupf.springbootjpa.dbobject;
import lombok.Data;
import javax.persistence.*;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@Data
@Entity
public class ClassInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(insertable = false, updatable = false)
private String className;
// @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
// @JoinColumn(name = "id")
// private List<StudentInfo> studentInfos;
}
档案对象
package com.lupf.springbootjpa.dbobject;
import lombok.Data;
import javax.persistence.*;
import java.util.Objects;
@Data
@Entity
public class FileInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private Integer fileId;
}
学生及科目关系映射对象
package com.lupf.springbootjpa.dbobject;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
public class SubjectSelectionInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "student_id")
private Integer studentId;
@Column(name = "subject_id")
private Integer subjectId;
@ManyToOne(cascade = CascadeType.ALL)
//name在表中的列
@JoinColumn(name = "student_id", referencedColumnName = "id", insertable = false, updatable = false)
private StudentInfo studentInfo;
@ManyToOne(cascade = CascadeType.ALL)
//name在表中的列
@JoinColumn(name = "subject_id", referencedColumnName = "id", insertable = false, updatable = false)
private SubjectInfo subjectInfo;
}
操作及测试
学生操作及测试
StudentRepository
@Repository
public interface StudentRepository extends JpaRepository<StudentInfo, Integer> {
//根据班级ID查询学生信息
@Query(value = "select a.* from student_info as a where stu_class_id = ?1", nativeQuery = true)
List<StudentInfo> findByStuClassId(Integer classId);
}
StudentService
package com.lupf.springbootjpa.service;
import com.lupf.springbootjpa.dbobject.StudentInfo;
import java.util.List;
/**
* 学生的Service
*/
public interface StudentService {
//添加学生 并添加学生的档案信息、分班信息以及学生选择的科目
StudentInfo save(StudentInfo studentInfo);
//查询某个学生的信息
StudentInfo findById(Integer id);
//删除学生信息
void deleteById(Integer id);
//修改信息
StudentInfo update(StudentInfo studentInfo);
//根据班级名称查询学生信息
List<StudentInfo> findByStuClassId(Integer classId);
}
StudentServiceImpl
package com.lupf.springbootjpa.service.impl;
import com.lupf.springbootjpa.dbobject.StudentInfo;
import com.lupf.springbootjpa.repository.StudentRepository;
import com.lupf.springbootjpa.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
StudentRepository studentRepository;
@Override
public StudentInfo findById(Integer id) {
Optional<StudentInfo> studentInfoOptional = studentRepository.findById(id);
if (null != studentInfoOptional && studentInfoOptional.isPresent())
return studentInfoOptional.get();
return null;
}
@Override
public void deleteById(Integer id) {
studentRepository.deleteById(id);
}
@Override
public StudentInfo update(StudentInfo studentInfo) {
return studentRepository.save(studentInfo);
}
@Override
public List<StudentInfo> findByStuClassId(Integer classId) {
List<StudentInfo> studentInfos = studentRepository.findByStuClassId(classId);
return studentInfos;
}
@Override
public StudentInfo save(StudentInfo studentInfo) {
StudentInfo saveStudentInfo = studentRepository.save(studentInfo);
return saveStudentInfo;
}
}
学生测试用例
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class StudentServiceImplTest {
@Autowired
StudentService studentService;
@Test
public void findById() {
//查询学生信息会一并查处用户所属的班级信息及选择的科目信息
StudentInfo studentInfo = studentService.findById(1);
log.info(JSON.toJSONString(studentInfo));
}
@Test
public void deleteById() {
//用户时会级联删除学生信息 学生档案信息 学生科目选择信息
studentService.deleteById(16);
}
@Test
public void update() {
StudentInfo studentInfo = studentService.findById(20);
studentInfo.setStuAge(8);
ClassInfo classInfo = new ClassInfo();
//这个classID必须在class_info表中存在,否则这里会报错
classInfo.setId(2);
studentInfo.setClassInfo(classInfo);
studentService.update(studentInfo);
}
@Test
public void save() {
StudentInfo studentInfo = new StudentInfo();
studentInfo.setStuAge(8);
studentInfo.setStuName("唐七");
studentInfo.setStuSex(1);
//级联更新 这里设置的id属性在添加学生信息的时候会一并将这部分数据添加到class_info表
ClassInfo classInfo = new ClassInfo();
//这个classID必须在class_info表中存在,否则这里会报错
classInfo.setId(2);
studentInfo.setClassInfo(classInfo);
//绑定并插入学生的用户信息
//这里是级联插入 添加的时候自动插入相关的档案信息
FileInfo fileInfo = new FileInfo();
fileInfo.setFileId(100004);
studentInfo.setFileInfo(fileInfo);
//级联更新学生选择的科目信息
//语文
SubjectInfo subjectInfo = new SubjectInfo();
//这里的ID必须在subject_info表中存在,否则这里会报错
subjectInfo.setId(1);
//数学
SubjectInfo subjectInfo2 = new SubjectInfo();
//这里的ID必须在subject_info表中存在,否则这里会报错
subjectInfo2.setId(2);
//英语
SubjectInfo subjectInfo3 = new SubjectInfo();
//这里的ID必须在subject_info表中存在,否则这里会报错
subjectInfo3.setId(3);
List<SubjectInfo> subjectInfos = new ArrayList<>();
subjectInfos.add(subjectInfo);
subjectInfos.add(subjectInfo2);
subjectInfos.add(subjectInfo3);
studentInfo.setSubjectInfos(subjectInfos);
studentService.save(studentInfo);
}
@Test
public void findByStuClassId() {
List<StudentInfo> s = studentService.findByStuClassId(2);
String va = JSON.toJSONString(s, SerializerFeature.DisableCircularReferenceDetect);
log.info(va);
}
}
测试详情
- 根据ID查询用户
- 插入测试
下图可见,插入学生信息的时候,同步插入了档案信息、班级信息及选择科目的关联关系
- 根据ID查询用户
- 修改测试
- 学生信息删除测试
删除学生的时候会同步删除档案信息及学生管理的科目信息
其他功能测试
其他对象为一些基础性的操作,没有什么特殊性,因此,这里就不占用篇幅,需要了解的可以下载源码查看。
常见问题
- could not initialize proxy - no Session
由于对象中管理了其他的对象,因此这里使用懒加载会出现未找到session的问题,需要将LAZY改为EAGER - java.lang.StackOverflowError
为Json反序列化导致的堆栈溢出
一般出现ManyToMany下,因为两个对象是你中有我,我中有你的关系,从而反序列化时导致递归堆栈溢出;
解决方案:可以使用@JSONField(serialize = false)注解忽略对应对象的反序列化 Multiple representations of the same entity […] are being merged. Detached: […]; Detached: […]; nested exception is java.lang.IllegalStateException: Multiple representations of the same entity […] are being merged. Detached: […]; Detached: […]
更新修改的时候,合并对象的问题
以上示例为将用户从其他班调整到id为3的班级,可能会报以上的错
解决方案:
在application.yml中添加以下配置spring:
jpa:
show-sql: true
properties:
hibernate:
event:
merge:
entity_copy_observer: allow
总结
到此,使用JPA操作关联表已经完了,可以看出,使用JPA,我们只需要关注于业务流程,几乎不用、或者很少去关注数据库那边的操作,从手动档直接换成了自动档,从而让我们有更多的精力去写业务层那一块儿的代码。
还没有评论,来说两句吧...