ShuSheng007

  • 首页
  • 关于
  • 联系
ShuSheng007
天行健,君子以自强不息 地势坤,君子以厚德载物
  1. 首页
  2. 数据库
  3. JPA
  4. 正文

Spring Data JAP多表关联关系详解

02/11/2023 772点热度 0人点赞 0条评论

[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007

@[toc]

概述

在Java程序访问关系型数据库这个领域,在国内使用最多的应该是MyBatis与MyBatisPlus,但是老外却特别中意JPA。我以前大多时候也是一直在使用MyBatis与MyBatisPlus,偶尔使用一点。最近公司项目使用了JPA,在使用过程中发现多表关联那块有点蒙,网上的中文资料还比较少,也有点不成体系,所以此处总结一下。

概念

JPA 全称为 Java persistence Api。是一套Java持久化规则,没有具体实现,Java在定义了JDBC的基础上又提供了更高层次的抽象 JPA,本意是统一各种ORM。因为我们目前主要使用Spring生态,所以这里谈论的内容是Spring实现的Jpa版本Spring Data Jpa 结合Hibernate 呈现的 。

Spring Data 是一个伞形项目,里面包含了大量与数据相关的项目,其中Spring Data JAP就是实践Java提出的标准JPA的项目,本文也是基于它实践的。

文本主要内容:

  • JPA 主键生成策略
  • JPA 多表关联
  • JPA多表关联时级联类型

主键生成类型

我们在创建JPA实体类的时候会被要求指定一个id,一般是数据表的主键。我们需要告诉数据库生成主键的策略,其使用GenerationType 枚举来表示,例如下面代码中指定主键生成策略为IDENTITY,那这些主键生成策略都有什么区别呢?

@Entity
@Table(name = "student", schema = "public")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Integer id;
}

下面是GenerationType源码,你可以看下能不能读懂注释,如果没有用过一般都是看不懂的。看不懂不要紧,下面我们举个例子就懂了

public enum GenerationType { 

    /**
     * Indicates that the persistence provider must assign 
     * primary keys for the entity using an underlying 
     * database table to ensure uniqueness.
     */
    TABLE, 

    /**
     * Indicates that the persistence provider must assign 
     * primary keys for the entity using a database sequence.
     */
    SEQUENCE, 

    /**
     * Indicates that the persistence provider must assign 
     * primary keys for the entity using a database identity column.
     */
    IDENTITY, 

    /**
     * Indicates that the persistence provider should pick an 
     * appropriate strategy for the particular database. The 
     * <code>AUTO</code> generation strategy may expect a database 
     * resource to exist, or it may attempt to create one. A vendor 
     * may provide documentation on how to create such resources 
     * in the event that it does not support schema generation 
     * or cannot create the schema resource at runtime.
     */
    AUTO
}

JPA(具体实现hibernate)是可以根据代码自动生成数据库表的,所以如果你愿意的话,开发的时候都不用自己建表。我们给主键设置不同的生成策略,然后看下JPA生成的sql是什么样的,我此次使用的数据库是PostgresSql。

  • TABLE

用单独的一张表来生成id

@Id
@GeneratedValue(strategy = GenerationType.TABLE)
@Column(name = "id", nullable = false)
private Integer id;

//创建student表
create table public.student (
       id int4 not null,
        stu_name varchar(255),
        primary key (id)
    )
//自动创建一张用于生成id的表    
create table hibernate_sequences (
       sequence_name varchar(255) not null,
        next_val int8,
        primary key (sequence_name)
    )
//给  hibernate_sequences表插入一条数据
insert into hibernate_sequences(sequence_name, next_val) values ('default',0)
------------------------插入数据-----------------------------
//先给hibernate_sequences表的某一行数据value加1
    select
        tbl.next_val 
    from
        hibernate_sequences tbl 
    where
        tbl.sequence_name=? for update
            of tbl
//更新回hibernate_sequences表
    update
        hibernate_sequences 
    set
        next_val=?  
    where
        next_val=? 
        and sequence_name=?

//获取新生成的id,然后给student表插入数据
    insert 
    into
        public.student
        (stu_name, id) 
    values
        (?, ?)
  • SEQUENCE

使用序列来生成主键id,这个概念有的数据库没有,例如mysql。

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "mySchSeq")
@SequenceGenerator(name = "mySchSeq", sequenceName = "school_seq")
@Column(name = "id", nullable = false)
private Integer id;

//创建表
    create table public.student (
       id int4 not null,
        stu_name varchar(255),
        primary key (id)
    )
 //生成一个 sequence,有些数据库没有这个概念,例如MySQL

// 插入数据,先从序列中获取一个id
    select
        nextval ('hibernate_sequence')
//插入数据
    insert 
    into
        public.student
        (stu_name, id) 
    values
        (?, ?)
  • IDENTITY

在postgreSQL 中还是通过生成一个序列来实现的:student_id_seq,但是与SEQUENCE那种方式还有一定的差别,student表会对生成的序列产生依赖,当删除表的时候,这个序列也就被删除了。MySql使用自增主键的方式,不论是在postgres还是mysql,建议使用这种。

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Integer id;

//创建表以及依赖的序列
    create table public.student (
       id int4 generated by default as identity,
        stu_name varchar(255),
        primary key (id)
    )
//插入数据
    insert 
    into
        public.student
        (stu_name) 
    values
        (?)
  • AUTO

由采用的数据库自己去决定,像postgresSQL用SEQUENCE,MySql用IDENTITY。下面是postgresSQL测试的情况,可见使用的是SEQUENCE类型。

    create table public.student (
       id int4 not null,
        stu_name varchar(255),
        primary key (id)
    )

    select
        nextval ('hibernate_sequence')
             : 
    insert 
    into
        public.student
        (stu_name, id) 
    values
        (?, ?)

关联关系

由于我们使用的是关系型数据库,例如MySql、PostgresSQL等,所以映射到领域对象时就会存在关联。还有一点必须要认识到:Spring data JPA中的的关联关系是由Hibernate实现的。

例如有一张student类,一张account类,一张school表,一张teacher表。

学生与账户是一对一关系,学生与当前学校是多对一关系,反过来学习与学生是一对多关系。学生与老师是多对多的关系,一个学生可以有语文老师,数学老师...,一个老师也会同时教很多学生。

JPA的关联关系又有双向和单向之分,如果双方都申明和对方的关系,那就是双向关联了,如果只有一方申明那就是单向关联了。

例如,学生和账户是一对一关系,如果只在Student里面申明拥有Account,那就是单向关联。如果我们同时在Account中也申明一个Student,那就是双向关联了。你可以从下面的代码中感受一下:

@Entity
@Table(name = "student", schema = "public")
public class Student {
    ...
    @OneToOne(cascade = {CascadeType.ALL})
    @JoinColumn(name = "account")
    private Account account;
}

//单向关联
@Entity
@Table(name = "account")
public class Account {
     ...
}

//双向关联
@Entity
@Table(name = "account")
public class Account {
    @OneToOne(mappedBy = "account",cascade = CascadeType.ALL)
    private Student student;
}

关联后,操作其中一方,就会自动操作另一方。例如我们查询Student的时候其Account属性也会自动被查询出来的。当我们用Mybatis的时候就比较麻烦点,通常我们会为每张表建立一个DAO,然后先查student表得到account的id,再查account,最后转换成我们的领域对象。或者手动join两张表,然后再转换成领域对象。

值得一提的是,关联关系是有主次之分的。例如Student和Account,我们一般认为Student拥有Account,所以我们会将account表的Id作为外键存储在student表里面。然后将Student作为聚合根来操作,如果采用DDD的设计模式的话,修改Account也是通过Student实现的。

那么单向关联和双休关联有什么区别呢?

单向关联:你可以在查询student的时候会自动将account也查出来,反之不行。
双向关联:你即可以在查询student的时候自动将account查出来了,也可以在查询account时将其关联的student查出来。

OneToOne

学生与账户是一对一关系,每个学生有一个唯一的账户,一个账户也只属于一个学生。

  • 单向关联
@Entity
@Table(name = "student", schema = "public")
public class Student {
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "account_id")
    private Account account;
}

这里的student表关联了account表, student表里面有一列叫account_id,存放的是account表的id,作为外键。

  • 双向关联
@Entity
@Table(name = "account")
public class Account {
    @OneToOne(mappedBy = "account", cascade = CascadeType.ALL)
    private Student student;
}

同时给Account实体也加上Student的关联就变成了双向关联。那个mappedBy = "account"里的account是Student类里面对应的属性名称,在IDEA甚至可以导航。mappedBy将两边联系了起来,通过它找到了account,然后又通过account上面的@JoinColumn(name = "account_id") 就知道他们的关系了。

ManyToOne

一对多以多方为参照物来说就是多对一,例如学生与学校就是多对一关系。

  • 单向关联
@Entity
@Table(name = "student", schema = "public")
public class Student {
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "school_id")
    private School school;
  }

student表关联了school表,student表里面存在一列school_id,存放的是school表的id,作为外键。

  • 双向关联
@Entity
@Table(name = "school")
public class School {
    @OneToMany(mappedBy = "school", cascade = CascadeType.ALL)
    private List<Student> students;
}

请参照OneToOne的说明

OneToMany

学校与学生就是一对多关系,一个学校可以包含很多学生

  • 单向
@Entity
@Table(name = "school")
public class School {
    @OneToMany( cascade = CascadeType.ALL)
    @JoinColumn(name = "school_id")
    private List<Student> students;
}

注意那个@JoinColumn(name = "school_id")中的school_id是student表中的字段,是它的外键,不是school表的字段。是不是很奇怪?我最开始对@JoinColumn也很蒙圈,外键具体放在哪张表里面是要依照数据库设计的原则来的,不论一对多,还是多对一都是将一方的id作为外键存放在在多方的表中。一般情况下,我们会将@JoinColumn放在拥有外键那张表的实体里,像OneToOne与ManyToOne 都比较自然。如果你非要使用OneToMany的单向联合的话,就要注意那个name的列是存放在代表Many的那张表的。

  • 双向绑定

当双向绑定时,请使用上一步ManyToOne部分介绍的方法。

ManyToMany

学生与老师之间是多对多的关系。 王二狗同时有语文老师孔夫子,数学老师祖冲之...,而孔夫子也可能同时教学生王二狗,牛翠华...

  • 单向绑定
@Entity
@Table(name = "student", schema = "public")
public class Student {
    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "student_teacher_relation",
            joinColumns = @JoinColumn(name = "student_id"),
            inverseJoinColumns = @JoinColumn(name = "teacher_id"))
    private List<Teacher> teachers;
  }    

我们知道关系数据库的多对多关系要借助中间表实现的。所以我们使用@JoinTable来配置这张中间表。student_teacher_relation是中间表的名称,student_id是student表的id,teacher_id是teacher表的id。值得注意的是,这个注解可以省略,然后JPA会按照自己的规则来命名。

  • 双向绑定
@Entity
@Table(name = "teacher")
public class Teacher {
    @ManyToMany(mappedBy = "teachers",cascade = CascadeType.ALL)
    private List<Student> students;
}

和其他级联关系一样。

以上就是JPA中多种关联关系的情况了,他们都是由hibernate实现的,这块也认为是它的精华,也是我们用惯mybaitis人员最不适应的地方。

级联方式

在上面各种关联关系中,我们一直在给关联注解的cascade属性配置CascadeType.ALL,像下面这样。

@OneToMany( cascade = CascadeType.ALL)

其实这块也比较难以理解,反正我第一次使用的时候又是蒙圈的,有的小伙伴要问了:你怎么总蒙圈啊?因为长了一颗爱蒙圈的脑袋,哈哈...。言归正传,下面我们遛一遛几个常用的吧

public enum CascadeType { 

//所有操作
    ALL, 

//插入
    PERSIST, 

//修改
    MERGE, 

//删除
    REMOVE
 }

注意这个级联关系只有在非查询的情况下才起作用,查询的时候不涉及。级联其实很好理解,例如Student关联了Account,那我们给student表插入一条数据的话是不是需要一条account,此时你需要告诉JPA先帮你插入一条account数据,再插入一条student数据,像下面代码那样。

@Entity
@Table(name = "student", schema = "public")
public class Student {
    ...
    @OneToOne(cascade = {CascadeType.PERSIST})
    @JoinColumn(name = "account")
    private Account account;
}
  • PERSIST

给student表插入一条数据的时候,当account不为null,那么就需要这个级联类型,以便同时在account表中也插入一条数据

  • MERGE

修改student表中某条数据,同时account也有修改时,就需要这个级联类型,以便于同时修改account表中的那条数据。

如果你只设置了CascadeType.PERSIST,那么这个操作就不能完成。所以你需要

@OneToOne(cascade = {CascadeType.MERGE})
  • REMOVE

删除student表中某条数据,同时要删除account里对应的那条数据

设置什么要看你的需求,如果你既有插入操作,也有修改操作,还有移除,那么就设置为CascadeType.ALL。这块设置不对要报错的,如果你只想对student增删改,那么你可以不设置account的级联关系,但是在操作的时候将account属性对象设置为null。

总结

从上面讲到的关联关系你也可以看出,互联网企业偏爱mybatis是有自己的道理的,他们不允许将外键维护在数据库中,他们要控制精确查询某几个字段,他们要优化sql提高效率,说白了就是他们要自己控制sql的操作,不需要框架自己完成.... 但对于一般规模的应用来说,JPA其实很不错的。

源码

一如既往,你可以在Github上找到源码:jpa-demo

本作品采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可
标签: Jpa OneToMany OneToOne
最后更新:02/19/2023

shusheng007

never give up ,keep going!

打赏 点赞

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

版权 © 2021 shusheng007.top 享有所有版权.

Theme Kratos Made By Seaton Jiang

津ICP备17001709号