08、Spring Data JPA 实战 - 多表设计(一对多,多对多)

表之间的划分

数据库中多表之间存在着三种关系,如图所示

 

从图可以看出,系统设计的三种实体关系分别为:多对多一对多一对一关系。注意:一对多关系可以看为两种: 即一对多多对一。所以说四种更精确。

分析步骤

在实际开发中,我们数据库的表难免会有相互的关联关系,在操作表的时候就有可能会涉及到多张表的操作。而在这种实现了ORM思想的框架中(如JPA),可以让我们通过操作实体类就实现对数据库表的操作。所以今天我们的学习重点是:掌握配置实体之间的关联关系。

1、 第一步:首先确定两张表之间的关系

如果关系确定错了,后面做的所有操作就都不可能正确。

1、 第二步:在数据库中实现两张表的关系
2、 第三步:在实体类中描述出两个实体的关系
3、 第四步:配置出实体类和数据库表的关系映射(重点)

一对多

例子

我们采用的示例为客户和联系人。

客户:指的是一家公司,我们记为A。

联系人:指的是A公司中的员工。

在不考虑兼职的情况下,公司和员工的关系即为一对多。

表关系建立

在一对多关系中,我们习惯把一的一方称之为主表,把多的一方称之为从表。在数据库中建立一对多的关系,需要使用数据库的外键约束。

 

创建数据库表

/*创建客户表*/
CREATE TABLE cst_customer (
    cust_id bigint(32) NOT NULL AUTO_INCREMENT COMMENT '客户编号(主键)',
    cust_name varchar(32) NOT NULL COMMENT '客户名称(公司名称)',
    cust_source varchar(32) DEFAULT NULL COMMENT '客户信息来源',
    cust_industry varchar(32) DEFAULT NULL COMMENT '客户所属行业',
    cust_level varchar(32) DEFAULT NULL COMMENT '客户级别',
    cust_address varchar(128) DEFAULT NULL COMMENT '客户联系地址',
    cust_phone varchar(64) DEFAULT NULL COMMENT '客户联系电话',
    PRIMARY KEY (cust_id)
) ENGINE=InnoDB AUTO_INCREMENT=94 DEFAULT CHARSET=utf8;
/*创建联系人表*/
CREATE TABLE cst_linkman (
    lkm_id bigint(32) NOT NULL AUTO_INCREMENT COMMENT '联系人编号(主键)',
    lkm_name varchar(16) DEFAULT NULL COMMENT '联系人姓名',
    lkm_gender char(1) DEFAULT NULL COMMENT '联系人性别',
    lkm_phone varchar(16) DEFAULT NULL COMMENT '联系人办公电话',
    lkm_mobile varchar(16) DEFAULT NULL COMMENT '联系人手机',
    lkm_email varchar(64) DEFAULT NULL COMMENT '联系人邮箱',
    lkm_position varchar(16) DEFAULT NULL COMMENT '联系人职位',
    lkm_memo varchar(512) DEFAULT NULL COMMENT '联系人备注',
    lkm_cust_id bigint(32) NOT NULL COMMENT '客户id(外键)',
    PRIMARY KEY (lkm_id),
    KEY FK_cst_linkman_lkm_cust_id (lkm_cust_id),
    CONSTRAINT FK_cst_linkman_lkm_cust_id FOREIGN KEY (lkm_cust_id) REFERENCES cst_customer (cust_id) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

创建实体类映射

Customer#

/**
 * @author DDKK.COM 弟弟快看,程序员编程资料站
 * @Description 用户实体类
 */
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Table(name="cst_customer")
public class Customer implements Serializable  {
   
     
    /**
     * 客户编号(主键)
     */
    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY) //自增主键,默认是Auto
    @Column(name="cust_id")  //如果可以直接映射,这个注解不需要写
    private Long custId;

    /**
     * 客户名称(公司名称)
     */
    @Column(name="cust_name")
    private String custName;

    /**
     * 客户信息来源
     */
    @Column(name="cust_source")
    private String custSource;

    /**
     * 客户所属行业
     */
    @Column(name="cust_industry")
    private String custIndustry;

    /**
     * 客户级别
     */
    @Column(name="cust_level")
    private String custLevel;

    /**
     * 客户联系地址
     */
    @Column(name="cust_address")
    private String custAddress;

    /**
     * 客户联系电话
     */
    @Column(name="cust_phone")
    private String custPhone;

    /**
     * 联系人集合
     *
     * 配置多表一对多关系
     *      声明关系
     * 在客户实体类上(一的一方)添加了外键配置,所以对于客户而言,也具备了维护外键的作用
     */
    @OneToMany(mappedBy = "customer",cascade=CascadeType.ALL,fetch=FetchType.LAZY)
    //级联保存、更新、删除、刷新;延迟加载。当删除用户,会级联删除该用户的所有文章
    //拥有mappedBy注解的实体类为关系被维护端
    //mappedBy="customer"中的customer是LinkMan中的customer属性
    private Set<LinkMan> linkMans=new HashSet<>();

    @Override
    public String toString() {
   
     
        return "Customer{" +
                "custId=" + custId +
                ", custName='" + custName + '\'' +
                ", custSource='" + custSource + '\'' +
                ", custIndustry='" + custIndustry + '\'' +
                ", custLevel='" + custLevel + '\'' +
                ", custAddress='" + custAddress + '\'' +
                ", custPhone='" + custPhone + '\'' +
                ", linkMans=" + linkMans +
                '}';
    }
}

LinkMan#

/**
 * @author DDKK.COM 弟弟快看,程序员编程资料站
 * @Description 联系人实体类
 */
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Table(name="cst_linkman")
public class LinkMan implements Serializable  {
   
     

    /**
     * 联系人id
     */
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="lkm_id")
    private Long lkmId;

    /**
     * 联系人姓名
     */
    @Column(name="lkm_name")
    private String lkmName;

    /**
     * 联系人性别
     */
    @Column(name="lkm_gender")
    private String lkmGender;

    /**
     * 联系人办公电话
     */
    @Column(name="lkm_phone")
    private String lkmPhone;

    /**
     * 联系人手机
     */
    @Column(name="lkm_mobile")
    private String lkmMobile;

    /**
     * 联系人邮箱
     */
    @Column(name="lkm_email")
    private String lkmEmail;

    /**
     * 联系人职位
     */
    @Column(name="lkm_position")
    private String lkmPosition;

    /**
     * 联系人备注
     */
    @Column(name="lkm_memo")
    private String lkmMemo;

    /**
     *客户
     *
     * 配置多表多对一关系
     *      1.声明关系
     *      2.配置外键(中间表)
     *
     */
    @ManyToOne(targetEntity = Customer.class,cascade=CascadeType.ALL)
    @JoinColumn(name="lkm_cust_id")
    private Customer customer;

    @Override
    public String toString() {
   
     
        return "LinkMan{" +
                "lkmId=" + lkmId +
                ", lkmName='" + lkmName + '\'' +
                ", lkmGender='" + lkmGender + '\'' +
                ", lkmPhone='" + lkmPhone + '\'' +
                ", lkmMobile='" + lkmMobile + '\'' +
                ", lkmEmail='" + lkmEmail + '\'' +
                ", lkmPosition='" + lkmPosition + '\'' +
                ", lkmMemo='" + lkmMemo + '\'' +
                '}';
    }
}

创建数据库操作类

@Repository
public interface  CustomerRepository  extends JpaRepository<Customer,Long>, JpaSpecificationExecutor<Customer> {
   
     
}

@Repository
public interface LinkManRepository extends JpaRepository<LinkMan,Long>, JpaSpecificationExecutor<LinkMan> {
   
     
}

注解说明

  • @OneToMany:

作用:建立一对多的关系映射

属性:

  targetEntityClass:指定多的多方的类的字节码

  mappedBy:指定从表实体类中引用主表对象的名称。

  cascade:指定要使用的级联操作

  fetch:指定是否采用延迟加载

  orphanRemoval:是否使用孤儿删除

  • @ManyToOne

作用:建立多对一的关系

属性:

targetEntityClass:指定一的一方实体类字节码

cascade:指定要使用的级联操作

fetch:指定是否采用延迟加载

optional:关联是否可选。如果设置为false,则必须始终存在非空关系。

  • @JoinColumn

作用:用于定义主键字段和外键字段的对应关系。

属性:

  name:指定外键字段的名称

  referencedColumnName:指定引用主表的主键字段名称

  unique:是否唯一。默认值不唯一

  nullable:是否允许为空。默认值允许。

  insertable:是否允许插入。默认值允许。

  updatable:是否允许更新。默认值允许。

  columnDefinition:列的定义信息。

操作

@Test
@Transactional(rollbackFor = Exception.class)
@Rollback(false)
public void testSave(){
   
     
    //创建一个客户
    Customer customer=new Customer();
    customer.setCustName("百度");

    //创建一个联系人
    LinkMan linkMan=new LinkMan();
    linkMan.setLkmName("小李");

    //保存到客户集合中
    //customer.getLinkMans().add(linkMan);
    
    //保存客户到联系人
    linkMan.setCustomer(customer);
    //先插入客户信息再插入联系人信息
    this.customerRepository.save(customer);
    this.linkManRepository.save(linkMan);
}

删除

  • 删除从表数据:可以随时任意删除
  • 删除主表数据

有从表数据

1). 在默认情况下,它会把外键字段置为null,然后删除主表数据。如果在数据库的表 结构上,外键字段有非空约束,默认情况就会报错了。

2). 如果配置了放弃维护关联关系的权利,则不能删除(与外键字段是否允许为null, 没有关系)因为在删除时,它根本不会去更新从表的外键字段了。

3). 如果还想删除,使用级联删除引用

没有从表数据引用:随便删

级联操作

级联操作:指操作一个对象同时操作它的关联对象

使用方法:只需要在操作主体的注解上配置cascade

cascade:配置级联操作

  • CascadeType.MERGE 级联更新
  • CascadeType.PERSIST 级联保存:
  • CascadeType.REFRESH 级联刷新:
  • CascadeType.REMOVE 级联删除:
  • CascadeType.ALL 包含所有
@OneToMany(mappedBy = "customer",cascade=CascadeType.ALL,fetch=FetchType.LAZY)
private Set<LinkMan> linkMans=new HashSet<>();

级联删除

@Test
@Transactional(rollbackFor = Exception.class)
@Rollback(false)
public void testRemove(){
   
     
    //获取数据
    Customer customer = this.customerRepository.getOne(28L);

    //从主表中删除数据
    this.customerRepository.delete(customer);
}

出现的一个错误

 

👉解决办法:SpringDataJpa在一对多、多对多关系的级联操作时出现StackOverflowError(是真滴坑)

多对多

例子

我们采用的示例为用户和角色

用户:指的是咱们班的每一个同学。

角色:指的是咱们班同学的身份信息。

比如A同学,它是我的学生,其中有个身份就是学生,还是家里的孩子,那么他还有个身份是子女。

同时B同学,它也具有学生和子女的身份。

那么任何一个同学都可能具有多个身份。同时学生这个身份可以被多个同学所具有。

所以我们说,用户和角色之间的关系是多对多。

表关系建立

多对多的表关系建立靠的是中间表,其中用户表和中间表的关系是一对多,角色表和中间表的关系也是一对多,如下图所示:

 

创建实体类

Role#

/**
 * @author DDKK.COM 弟弟快看,程序员编程资料站
 * @Description 角色实体类
 */
@Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Table(name="sys_role")
public class Role {
   
     
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="role_id")
    private Long roleId;

    @Column(name="role_name")
    private String roleName;

    /**
     * 配置多对多关系
     * 被动的一方放弃维护权
     */
    //    @ManyToMany(targetEntity = User.class,cascade = CascadeType.ALL)
    //    @JoinTable(name="sys_user_role",
    //            //当前对象在中间表的外键
    //            joinColumns = {@JoinColumn(name="sys_role_id",referencedColumnName = "role_id")},
    //            //对方对象在中间表的外键
    //            inverseJoinColumns = {@JoinColumn(name="sys_user_id",referencedColumnName = "user_id")})
    @ManyToMany(mappedBy = "roles",cascade = CascadeType.ALL)
    private Set<User> users=new HashSet<>();

    @Override
    public String toString() {
   
     
        return "Role{" +
            "roleId=" + roleId +
            ", roleName='" + roleName + '\'' +
            '}';
    }
}

User#

/**
 * @author DDKK.COM 弟弟快看,程序员编程资料站
 * @Description 用户实体类
 */

@Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Table(name="sys_user")
public class User {
   
     

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="user_id")
    private Long userId;

    @Column(name="user_name")
    private String userName;

    @Column(name="age")
    private Integer age;

    /**
     * 配置多对多关系
     */
    @ManyToMany(targetEntity = Role.class,cascade = CascadeType.ALL)
    @JoinTable(name="sys_user_role",
               //当前对象在中间表的外键
               joinColumns = {
   
     @JoinColumn(name="sys_user_id",referencedColumnName = "user_id")},
               //对方对象在中间表的外键
               inverseJoinColumns = {
   
     @JoinColumn(name="sys_role_id",referencedColumnName = "role_id")})
    private Set<Role> roles=new HashSet<>();

    @Override
    public String toString() {
   
     
        return "User{" +
            "userId=" + userId +
            ", userName='" + userName + '\'' +
            ", age=" + age +
            ", roles=" + roles +
            '}';
    }
}

创建数据库操作类

@Repository
public interface RoleRepository extends JpaRepository<Role,Long>, JpaSpecificationExecutor<Role> {
   
     
}

@Repository
public interface UserRepository extends JpaRepository<User,Long>, JpaSpecificationExecutor<User> {
   
     
}

注解说明

  • @ManyToMany

作用:用于映射多对多关系

属性:

cascade:配置级联操作。

fetch:配置是否采用延迟加载。

 targetEntity:配置目标的实体类。映射多对多的时候不用写。

  • @JoinTable

作用:针对中间表的配置

属性:

name:配置中间表的名称

joinColumns:中间表的外键字段关联当前实体类所对应表的主键字段

inverseJoinColumn:中间表的外键字段关联对方表的主键字段

  • @JoinColumn

作用:用于定义主键字段和外键字段的对应关系。

属性:

name:指定外键字段的名称

referencedColumnName:指定引用主表的主键字段名称

unique:是否唯一。默认值不唯一

nullable:是否允许为空。默认值允许。

insertable:是否允许插入。默认值允许。

updatable:是否允许更新。默认值允许。

columnDefinition:列的定义信息。

操作

@Test
@Transactional(rollbackFor = Exception.class)
@Rollback(false)
public void testSave(){
   
     
    User user=new User();
    user.setUserName("小李");

    Role role=new Role();
    role.setRoleName("java程序员");

    //配置用户到角色关系,可以对中间表中的数据进行维护 1-1
    user.getRoles().add(role);

    //配置角色到用户关系,可以对中间表中的数据进行维护 1-1
    //        role.getUsers().add(user);

    this.userRepository.save(user);
    this.roleRepository.save(role);
}

注意

如果双向都设置关系,意味着双方都维护中间表,都会往中间表插入数据,中间表的2个字段又作为联合主键,所以报错,主键重复,解决保存失败的问题:只需要在任意一方放弃对中间表的维护权即可,推荐在被动的一方放弃

@ManyToMany(targetEntity = User.class,cascade = CascadeType.ALL)
@JoinTable(name="sys_user_role",
           //当前对象在中间表的外键
           joinColumns = {
   
     @JoinColumn(name="sys_role_id",referencedColumnName = "role_id")},
           //对方对象在中间表的外键
           inverseJoinColumns = {
   
     @JoinColumn(name="sys_user_id",referencedColumnName = "user_id")})
private Set<User> users=new HashSet<>();

改为:

@ManyToMany(mappedBy = "roles",cascade = CascadeType.ALL)
private Set<User> users=new HashSet<>();

级联删除

@Test
@Transactional(rollbackFor = Exception.class)
@Rollback(false)
public void testDelete(){
   
     
    this.userRepository.deleteById(1L);
}