SpringBoot学习之旅(八)---JPA进阶篇之联表操作

前言

前两节有说道JPA的基础操作及JPA自定义查询、修改、分页等操作,有兴趣的可以去看看,前面两章的所有操作都只是在操作一张表,但是真实的项目中,不可能所有的业务都只去做单标的操作,因为这不符合数据库表的设计法则,因此,各个表之间就总会发生那么点正当、或者不正当的关系;那么我们就得去理清各个要素之间的关系;

源码下载

点击开源中国下载源码

由于代码内容比较多多,建议优先下载代码,对着代码读更加快捷

其他文章

SpringBoot学习之旅(七)—JPA进阶篇之自定义查询、修改、分页


数据关联关系映射
关系类型Owning-SideInverse-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查询用户
    • 插入测试
      下图可见,插入学生信息的时候,同步插入了档案信息、班级信息及选择科目的关联关系


  • 修改测试

  • 学生信息删除测试
    删除学生的时候会同步删除档案信息及学生管理的科目信息
其他功能测试

其他对象为一些基础性的操作,没有什么特殊性,因此,这里就不占用篇幅,需要了解的可以下载源码查看。

常见问题
  • 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,我们只需要关注于业务流程,几乎不用、或者很少去关注数据库那边的操作,从手动档直接换成了自动档,从而让我们有更多的精力去写业务层那一块儿的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一行Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值