美文网首页基本功框架学习Java学习笔记
JPA实体关系映射:@ManyToMany多对多关系、@OneT

JPA实体关系映射:@ManyToMany多对多关系、@OneT

作者: 三汪 | 来源:发表于2017-08-01 16:23 被阅读4206次

    本文由作者三汪首发于简书。


    为什么要有实体关系映射

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

    什么是多对多关系

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

    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为被维护方。

    但是在实际操作中,我发现其实被维护方于维护方的概念并不那么重要。被维护方也可以对双方关系进行维护。下面通过一组测试用例来进行说明。
    (关于mappedBy,我又更新了一篇补遗,建议阅读。阅读时间3分钟 ==》戳这里)
    测试用例

        /**
         * 仅将被维护方对象添加进维护方对象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>


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

    相关文章

      网友评论

      • 563aabcef078:被控方查询,中间表存在关联关系,会直接报错,这个要怎么解决
      • 造一个大大的轮子:你的博文存在问题:注意,你在关系被维护方设置了级联操作(CascadeType.ALL).我举个简单的例子中,你保存了course对象,但是course放弃了维护关系的权利,它不会在中间表中插入记录,进而也不会在维护方表中插入数据.亲测,Hibernate中JPA操作(多对多关系)
        三汪:对不起。我有点没有看懂你的意思。可以重新组织一下语言吗。你说的不会在中间表中插入记录是指`多对多插入2()`吗。如果是的话这个不存在问题呀。我在下面的测试说明和结果中有说明。实际情况是,只要把被维护方set进维护方的关联集合中。不论你save()的是维护方还是被维护方,都会产生级联保存和中间表关联。这也是我这篇博文想要阐述的问题。
      • LxjAzni丶7:沙发:octocat:
        三汪:@LxjAzni丶7 这种方式不够灵活。我自定义了一个注解在Controller层作过滤。你可以参考https://my.oschina.net/diamondfsd/blog/836727
        LxjAzni丶7:@三汪 我这边manytomany会出现json序列化的死循环 在被维护类的属性上加了@JsonBackReference就可以了
        三汪::smile: 好雅致

      本文标题:JPA实体关系映射:@ManyToMany多对多关系、@OneT

      本文链接:https://www.haomeiwen.com/subject/xgujlxtx.html