Spring Data JAP多表关联关系详解

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

概述

在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_idstudent表中的字段,是它的外键,不是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其实很不错的。

源码

一如既往,你可以在首发文章末尾找到源码:Spring Data JAP多表关联关系详解

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ShuSheng007

亲爱的猿猿,难道你又要白嫖?

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

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

打赏作者

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

抵扣说明:

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

余额充值