Osheep

时光不回头,当下最重要。

JPA实体关系映射:@ManyToMany多对多关系、@OneToMany@ManyToOne一对多多对一关系和@OneToOne的深度实例解析。

为什么要有实体关系映射

答:简化编程操作。把冗余的操作交给底层框架来处理。
例如,如果我要给一位新入学的学生添加一位新的老师。而这个老师又是新来的,在学生数据库与教师数据库中均不存在对应的数据。那么我需要先在教师数据库中保存新来的老师的数据,同时在学生数据库中保存新学生的数据,然后再给两者建立关联。
而如果我们使用了实体关系映射,我们只需要将该新教师实体交给该学生实体,然后保存该学生实体即可完成。

什么是多对多关系

多对多关系是关系数据库中两个表之间的一种关系, 该关系中第一个表中的一个行可以与第二个表中的一个或多个行相关。第二个表中的一个行也可以与第一个表中的一个或多个行相关。
如果我们通过学生与课程的关系来说明多对多关系:一位学生,会修多门课程;而一门课程,也会被多位学生修习。此时,双方的关系即为多对多关系。
拥有多对多关系的两个实体将会有一个中间表来记录两者之间的关联关系。
下面,我们来建立实体。

Studnt实体

package com.wolfgy.domain;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

import org.hibernate.annotations.GenericGenerator;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Student {

    @Id
    @GeneratedValue(generator = "idGenerator")
    @GenericGenerator(name = "idGenerator", strategy = "uuid")
    private String id;
    private String sName;
    @ManyToMany(cascade=CascadeType.ALL,fetch=FetchType.LAZY)
    private Set<Course> courses = new HashSet<>();
}

Course实体

package com.wolfgy.domain;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

import org.hibernate.annotations.GenericGenerator;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Course {
    @Id
    @GeneratedValue(generator = "idGenerator")
    @GenericGenerator(name = "idGenerator", strategy = "uuid")
    private String id;
    private String cName;
    @ManyToMany(cascade=CascadeType.ALL,fetch=FetchType.LAZY,mappedBy="courses")
    private Set<Student> students= new HashSet<>();
}

@ManyToMany注解说明:

如代码所示,在两个实体中,我们都使用了@ManyToMany这一注解。
这一注解表明,当前实体为多对多关系的其中一端。

注解可以在Collection、Set、List、Map上使用,我们可以根据业务需要选择。
Collection类是Set和List的父类,在未确定使用Set或List时可使用;
Set集合中对象不能重复,并且是无序的;
List集合中的对象可以有重复,并且可以有排序;
Map集合是带有key和value值的集合。

同时,我们声明的集合需要进行初始化。
如Collection可以初始化为ArrayList或HashSet;
Set可以初始化为HashSet;
List可以初始化为ArrayList;
Map可以初始化为HashMap。

在注解中,我们可以设置cascade(级联关系),fetch(加载策略),mappedBy(声明关系的维护方)等属性。
关于级联关系可以在我的这篇文章中了解:戳这里
我们简要介绍一下mappedBy。

mappedBy声明于关系的被维护方,声明的值为关系的维护方的关系对象属性名。
在实例中,mappedBy被声明于Course类中,其值为Student类中的Set对象”courses”。即,Student为关系维护方,Course为被维护方。

但是在实际操作中,我发现其实被维护方于维护方的概念并不那么重要。被维护方也可以对双方关系进行维护。下面通过一组测试用例来进行说明。

测试用例

    /**
     * 仅将被维护方对象添加进维护方对象Set中
     * 保存维护方对象
     */
    @Test
    public void 多对多插入1() {
        Student s = new Student();
        s.setSName("二狗");
        Course c = new Course();
        c.setCName("语文");
        s.getCourses().add(c);
        studentService.save(s);
    }
    
    /**
     * 仅将维护方对象添加进被维护方对象Set中
     * 保存被维护方对象
     */
    @Test
    public void 多对多插入2() {
        Student s = new Student();
        s.setSName("三汪");
        Course c = new Course();
        c.setCName("英语");
        c.getStudents().add(s);
        courseService.save(c);
    }
    
    /**
     * 将双方对象均添加进双方Set中
     * 保存被维护方对象
     */
    @Test
    public void 多对多插入3() {
        Student s = new Student();
        s.setSName("一晌");
        Course c = new Course();
        c.setCName("数学");
        s.getCourses().add(c);
        c.getStudents().add(s);
        courseService.save(c);
    }

    /**
     * 删除维护方对象
     */
    @Test
    public void 多对多删除1(){
        Student s = studentService.findByName("二狗");
        studentService.delete(s);
    }

    /**
     * 删除被维护方对象
     */
    @Test
    public void 多对多删除2(){
        //Course c = courseService.findByName("英语");
        Course c = courseService.findByName("数学");
        courseService.delete(c);
    }
测试说明及结果:

在上面的测试用例中,我们进行了三次不同的保存和三次不同的保存删除操作(多对多删除2中分别进行了两次删除操作),分别对应二狗:语文三汪:英语一晌:数学三组数据。

  • 第一组数据(仅将被维护方对象添加进维护方对象Set中,对维护方对象的单独保存和删除):由于操作对象是维护方,成功地在student、course以及中间表student_courses中分别添加了数据并成功进行了删除。若将删除对象换成被维护方,同样能够成功删除。
  • 第二组数据(仅将维护方对象添加进被维护方对象Set中,对被维护方对象的单独保存和删除):操作对象在这里换成了被维护方。不负众望,出问题了。保存的时候,student表和course表倒是都成功地插入了数据,但是中间表中,并未产生对两者数据的关联。因此,在删除的时候也只删除了course中的数据。
  • 第三组数据( 将双方对象均添加进双方Set中,对被维护方对象进行保存和删除):操作对象是被维护方,操作结果与第一组相同。

由此可知,实际操作中,只要中间表建立了关联,即使是注解定义的被维护方也是可以对双方关系进行维护的。

一对多、多对一与一对一关系的介绍

当我们了解完多对多关系以后,再来了解这三种关系映射就简单了许多。原理与多对多关系都是相同的,下面将简要介绍其不同之处。

一对多关系与多对一关系
  • 一对多关系即数据库中的一行数据关联另一个数据库中的多行关系。多对一与之相反。
  • 一对多与多对一关系也可能会有中间表关联两者。但是我们一般不建议使用中间表。使用mapperBy可以避免系统生成中间表(会在多的一方数据库中增加一个字段记录外键)。
  • 这两个关系中的mappedBy一般声明于一的一方,即一的一方为被维护方。

声明示例:

public class Student {
    @ManyToOne(cascade=CascadeType.ALL,fetch=FetchType.LAZY)
    private ClassEntity classEntity;
    //其余略
}
public class ClassEntity {
    @OneToMany(cascade=CascadeType.PERSIST,fetch=FetchType.LAZY,mappedBy="classEntity")
    private Set<Student> students= new HashSet<>();
    //其余略
}
一对一关系
  • 一对一关系即两个数据库中的数据一一对应。
    其他就没有什么需要额外介绍的了,原理与上面的是关系映射一样的。
    声明示例:
public class NewsResourceEntity{  
    @OneToOne(optional = false, cascade = CascadeType.MERGE)  
    private ResourceEntity resource;  
    //其余略
} 
public class ResourceEntity {  
    @OneToOne(optional = true, cascade = CascadeType.ALL, fetch=FetchType.LAZY, mappedBy = "resource")  
    private NewsResourceEntity newsResource;  
//其余略
} 

单向与双向关联的简介

本文从头到尾所有的示例,使用的都是双向关联。即在关联双方都进行关联声明。而事实上,除了双向关联,还有一种用法是单向关联。即在关联的其中一方进行关联。
下面进行介绍():

当使用单向关联时,由父类管理关联关系,子类无法管理,而这时,父亲知道自己的儿子,但是,从儿子对象不知道父亲是谁。
单向关联时,只指定<one-to-many>
当使用双向关联时,关联关系的管理可以通过inverse指定,这时,儿子能清楚的知道自己的父亲是谁。 双向关联时,还要指定<many-to-one>


以上。
希望我的文章对你能有所帮助。
我不能保证文中所有说法的百分百正确,但我能保证它们都是我的理解和感悟以及拒绝复制黏贴。
有什么意见、见解或疑惑,欢迎留言讨论。

点赞