Springboot中jpa中一对多关系

574 阅读4分钟

前言

在使用jpa持久化框架中,经常要建立关系表,jpa提供级联注解,这里说说其中的一对多@OneToMany和多对一@ManyToOne关系

注解说明

@OneToMany注解来定义具有一对多的多值关联,比如学校和和学生的关系
@ManyToOne注解来定义具有多对一的多值关联,比如学生和和学校的关系

使用说明

(1)建立一个学校实体类

@Data
@Entity
@Table(name = "t_school")
@Accessors(chain = true)
public class School {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "school")
    private List<Student> studentList;
}

(2)建立一个学生实体类

@Data
@Entity
@Table(name = "t_student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "school_id")
    private School school;

}

备注:如果关系是双向的,则@OneToMany必须使用 mappedBy 元素来指定作为关系所有者的实体的关系字段或属性,否则jpa在自动建表的时候,会生成另外一张表

image.png

使用mappedBy映射之后,自动生成表只会生成两张表

image.png

@JoinColumn标识,外键关系维护在t_student数据库表

image.png

前提

Student和School中dao层存在

@Repository
public interface SchoolRepository extends JpaRepository<School, Long>,
        JpaSpecificationExecutor<School> {

}
@Repository
public interface StudentRepository extends JpaRepository<Student, Long>,
        JpaSpecificationExecutor<Student> {

}

controller写法

@Slf4j
@RestController
@RequestMapping("/school")
public class SchoolController {


    @Autowired
    private SchoolRepository schoolRepository;


    @Autowired
    private StudentRepository studentRepository;


    @GetMapping("/save")
    public void save() {
        School school = new School();
        school.setName("aaaaa");
        schoolRepository.save(school);
    }

    @GetMapping("/save1")
    public void save1() {
        Student student = new Student();
        student.setName("aaaa");
        studentRepository.save(student);
    }


    /**
     * 保存学生和学校之间的外键关系
     */
    @GetMapping("/save2")
    public void save2(@RequestParam("studentId") Long studentId, @RequestParam("schoolId") Long schoolId) {
        studentRepository.findById(studentId).ifPresent(e -> schoolRepository.findById(schoolId).ifPresent(x -> {
            e.setSchool(x);
            studentRepository.save(e);
        }));
    }


    /**
     * 断开学生和学校之间的外键关系
     */
    @GetMapping("/save3")
    public void save3(@RequestParam("id") Long id) {
        studentRepository.findById(id).ifPresent(e -> {
            e.setSchool(null);
            studentRepository.save(e);
        });
    }


    /**
     * 删除学生数据
     */
    @GetMapping("/deleteStudentById")
    public void deleteStudentById(@RequestParam("id") Long id) {
        studentRepository.deleteById(id);
    }


    /**
     * 删除学校数据
     */
    @GetMapping("/deleteSchoolById")
    public void deleteSchoolById(@RequestParam("id") Long id) {
        schoolRepository.deleteById(id);
    }


}

(1)调用save接口,保存学校数据,数据为:

image.png

(2)调用save1接口,保存学生数据,数据为:

image.png (3)调用save2关联两张表外键关系

image.png

(4)调用save3接口,可以断开两者之间的外键关系

image.png

(5)调用deleteStudentById接口,可以单独删除学生表数据

image.png 这时候学生表数据不见了,学校数据还在

image.png

(6)但这时候,如果学校和学生数据还存在外键关联,这时候删除学校数据,则会报错

image.png 报存在关键关系,不给删除,这个时候,我们要删除学校数据怎么办?就是先断开学校和学生的外键关系,然后再删除学校数据

/**
 * 断开学生和学校之间的外键关系
 */
@GetMapping("/deleteSchoolById1")
public void deleteSchoolById1(@RequestParam("id") Long id) {
    schoolRepository.findById(id).ifPresent(e -> {
        e.getStudentList().forEach(s -> {
            s.setSchool(null);
            studentRepository.save(s);
        });
    });
    schoolRepository.deleteById(id);
}

image.png

image.png 备注:

@OneToMany注解有个orphanRemoval字段,用于标识删除一方(One)数据的同时是否删除掉多方(Many)的数据,默认是false,当设置为true的时候,删除学校,就不会报错了,就会把学生关联数据一起删除

image.pngorphanRemoval设置为true,这个时候,删除学校数据,就会把学生表一起删除

image.png

image.png

@ManyToOne有个optional字段,关联是否为可选,如果设置为 false,则必须始终存在非空关系。默认为 true

CascadeType字段

CascadeType有多个类型

CascadeType.ALL:所有操作都会级联执行,包括保存(persist)、更新(merge)、删除(remove)等。
CascadeType.PERSIST:级联保存操作。
CascadeType.MERGE:级联更新操作。
CascadeType.REMOVE:级联删除操作。
CascadeType.REFRESH:级联刷新操作。
CascadeType.DETACH:级联脱管操作。

以以上School实体类为例,

@Data
@Entity
@Table(name = "t_school")
@Accessors(chain = true)
public class School {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "school", cascade = CascadeType.ALL)
    private List<Student> studentList;
}

Student实体类

@Data
@Entity
@Table(name = "t_student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "school_id")
    private School school;

}

就代表School的增删改查,影响Student类,这个时候,可以级联保存了

controller写法

@Slf4j
@RestController
@RequestMapping("/student")
public class StudentController {


    @Autowired
    private SchoolRepository schoolRepository;


    @Autowired
    private StudentRepository studentRepository;


    @GetMapping("/save")
    public void save() {
        School school = new School();
        school.setName("hello");
        Student student = new Student();
        student.setName("aa1");
        List<Student> studentList = new ArrayList<>(10);
        studentList.add(student);
        studentList.forEach(e->{
            e.setSchool(school);
        });
        school.setStudentList(studentList);
        schoolRepository.save(school);
    }


    /**
     * 删除学校数据
     */
    @GetMapping("/deleteSchoolById")
    public void deleteSchoolById(@RequestParam("id") Long id) {
        schoolRepository.deleteById(id);
    }


    /**
     * 删除学生数据
     */
    @GetMapping("/deleteStudentById")
    public void deleteStudentById(@RequestParam("id") Long id) {
        studentRepository.deleteById(id);
    }

}

(1)执行save接口,会同时保存SchoolStudent数据,

image.png

image.png

备注:这里需要注意的是,要把School实体类塞到Student实体类,否则不会级联保存

(2)执行deleteSchoolById接口,会同时删除SchoolStudent数据。因为School使用了ALL,也就是所有操作都会级联影响Student数据

image.png (3)执行deleteStudentById接口,只会删除Student数据,因为Student没有级联

image.png

(4)如果想断开学生表外键,可以使用

/**
 * 断开学生表外键
 */
@GetMapping("/hello")
public void hello(@RequestParam("id") Long id) {
    schoolRepository.findById(id).ifPresent(e -> {
        e.getStudentList().forEach(s -> {
            s.setSchool(null);
            studentRepository.save(s);
        });
    });
}

这个时候再执行接口删除,就不会删除学生表了,但是要注意的是,要分两个接口调用

(5)如果想学生表增删等,也影响学校表,那就一起级联

@Data
@Entity
@Table(name = "t_student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "school_id")
    private School school;

}

总结

jpa中的一对多和多对一如果相对熟悉的话,可以推荐使用,但是一般在开发过程中,因人而异,大部分不会采用外键级联表,这种根据自己技术决定