JPA三:关联映射(一)

1 环境搭建

  1. SQL脚本【可以不执行下面SQL、仅备用而已,因为JPA可以根据配置的实体类自动创建表

    -- 创建客户表
    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;
    
  2. 创建普通maven工程,添加依赖

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    
    <dependencies>
        <!-- spring-boot 测试类依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.5.6</version>
        </dependency>
        <!-- jpa 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>2.6.0</version>
        </dependency>
        <!-- mysql 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>
        <!-- lombok 依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
    </dependencies>
    
  3. 配置文件 application.yml

    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password: root
        url: jdbc:mysql://192.168.192.134:3306/test?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
      jpa:
        show-sql: true
        hibernate:
          # ddl-auto: update
          ddl-auto: create
    
    # jpa.hibernate.ddl-auto 说明
    # create:每次都会重新创建数据库表
    # update:有表不会重新创建,没有表会重新创建表
    # 注
    # 1. 测试一对多时,为了防止以前数据造成影响,设置为create,即每次测试时重新建表
    # 2. 测试一对多级联删除时,改成update,否则若是重新建表数据库中没有数据!(注:测试级联删除之前运行级联添加方法造一些数据)
    # 3. 测试多对多,没写sql语句需要jpa自己创建,为了防止上次结果造成影响,因此改为create
    # 4. 测试多对多级联删除时,改成update,否则若是重新建表数据库中没有数据!(注:测试多对多级联删除之前运行级联添加方法造一些数据)
    # 5. 测试对象导航查询,改为update(自己去数据库中造一些数据)
    
  4. 实体类

    • JPA 中使用 lombok 的 “@Data” 注解潜在一个巨坑!!!暂时先不解决,见下文 “2.1 编码” 中的测试类 testSave3()
    package cn.entity;
    import lombok.Data;
    import javax.persistence.*;
    
    @Entity
    @Table(name = "cst_customer")
    @Data
    public class CustomerEntity {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @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;
    }
    
    package cn.entity;
    import lombok.Data;
    import javax.persistence.*;
    
    @Entity
    @Table(name = "cst_linkman")
    @Data
    public class LinkManEntity {
    
        @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; // 联系人备注
    }
    
    
  5. 持久层

    package cn.repository;
    import cn.entity.CustomerEntity;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface CustomerRepository extends JpaRepository<CustomerEntity,Long> {
    }
    
    package cn.repository;
    import cn.entity.LinkManEntity;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface LinkManRepository extends JpaRepository<LinkManEntity, Long> {
    }
    
  6. 启动类

    package cn;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class App {
        public static void main(String[] args) {
            SpringApplication.run(App.class, args);
        }
    }
    
  7. 环境测试

    package test;
    
    import cn.App;
    import cn.entity.CustomerEntity;
    import cn.repository.CustomerRepository;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import java.util.List;
    
    @SpringBootTest(classes = App.class)
    public class TestEnv {
        @Autowired
        private CustomerRepository customerRepository;
    
       /**
         * 环境测试
         */
        @Test
        public void testEnv() {
            List<CustomerEntity> list = customerRepository.findAll();
            list.forEach(customer -> System.out.println(customer));
            // 此时结果为空,因为数据库中根本就没有插入数据
    
            /**
             * 注:若 application.yml 中 jpa.hibernate.ddl-auto=create,则会看到JPA框架会根据实体类配置自动创建数据库中的表
             *
             * DDL SQL
             * Hibernate: drop table if exists cst_customer
             * Hibernate: drop table if exists cst_linkman
             * Hibernate: create table cst_customer (cust_id bigint not null auto_increment, cust_address varchar(255), cust_industry varchar(255), cust_level varchar(255), cust_name varchar(255), cust_phone varchar(255), cust_source varchar(255), primary key (cust_id)) engine=InnoDB
             * Hibernate: create table cst_linkman (lkm_id bigint not null auto_increment, lkm_email varchar(255), lkm_gender varchar(255), lkm_memo varchar(255), lkm_mobile varchar(255), lkm_name varchar(255), lkm_phone varchar(255), lkm_position varchar(255), lkm_cust_id bigint, primary key (lkm_id)) engine=InnoDB
             * Hibernate: alter table cst_linkman add constraint FKh9yp1nql5227xxcopuxqx2e7q foreign key (lkm_cust_id) references cst_customer (cust_id)
             *
             * 查询SQL
             * Hibernate: select customeren0_.cust_id as cust_id1_0_, customeren0_.cust_address as cust_add2_0_, customeren0_.cust_industry as cust_ind3_0_, customeren0_.cust_level as cust_lev4_0_, customeren0_.cust_name as cust_nam5_0_, customeren0_.cust_phone as cust_pho6_0_, customeren0_.cust_source as cust_sou7_0_ from cst_customer customeren0_
             */
        }
    }
    
  • 项目结构如下

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aX7REgOq-1641649801696)(assets/image-20220107171318982.png)]

  • 特别说明:关联映射包含三种关系,即一对一、一对多、多对多。但在实际开发中,一对一比较简单且基本上很少用到,因此本文主要讲一对多和多对多

2 一对多

概述

  1. 一对多关系分为两种,即一对多、多对一。

  2. 概念

    1. 主表:"一"的一方
    2. 从表:"多"的一方
    3. 外键:需要在从表上新建一列作为外键,他的取值来源于主表的主键
    • 总结:即在“多”的一方引入“一”的一方的主键作为外键

分析步骤

  • 案例:客户和联系人
  1. 明确表关系
    • 客户可以理解为"一家公司",联系人可以理解为"一家公司的员工",因此客户和联系人为一对多的关系
  2. 确定表关系(描述 外键或中间表,一对多用外键、多对多用中间表)
    1. 主表:客户表("一"的一方)
    2. 从表:联系人表("多"的一方)
    3. 因此需要在从表上加外键
  3. 编写实体类,在实体类中描述表关系(即包含关系)
    1. 客户:在客户的实体类中包含一个联系人的集合;
    2. 联系人:在联系人的实体类中包含一个客户对象.
  4. 配置映射关系
    • 使用JPA注解配置一对多映射关系

2.1 添加

  • 在上面环境的基础上进行修改
  1. 修改实体类

    package cn.entity;
    
    
    import lombok.Getter;
    import lombok.Setter;
    import javax.persistence.*;
    import java.util.HashSet;
    import java.util.Set;
    
    @Entity
    @Table(name = "cst_customer")
    //@Data
    @Getter
    @Setter
    public class CustomerEntity {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @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和@ManyToOne注解
         */
    //    @OneToMany(targetEntity = LinkManEntity.class)
    //    @JoinColumn(name = "lkm_cust_id", referencedColumnName = "cust_id")
    //    private Set<LinkManEntity> linkMans = new HashSet<LinkManEntity>();
    
        /**
         * "一"的一方放弃外键维护权(此时就不会出现多余的更新外键update语句)
         * mappedBy:对方配置关系的属性名称(即参照对方 多对一@ManyToOne的配置)
         */
        @OneToMany(mappedBy = "customer")
        private Set<LinkManEntity> linkMans = new HashSet<LinkManEntity>();
    }
    
    
    package cn.entity;
    
    import lombok.Getter;
    import lombok.Setter;
    
    import javax.persistence.*;
    
    @Entity
    @Table(name = "cst_linkman")
    //@Data
    @Getter
    @Setter
    public class LinkManEntity {
    
        @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; // 联系人备注
    
        /*-------------------------------------- 一对多 --------------------------------------*/
        /**
         * @JoinColumn 注解用于配置 外键(一对多、多对一)或中间表(多对多)
         * name:外键字段名称
         * referencedColumnName:主表的主键名称
         * <p>
         * 这些配置看创建数据库的SQL语句(若是没SQL则会根据配置自动建表)
         * 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
         * 因为name配置外键,故name="lkm_cust_id"
         * 主表主键:PRIMARY KEY (`cust_id`), 因此referencedColumnName = "cust_id"
         */
        @ManyToOne(targetEntity = CustomerEntity.class)
        @JoinColumn(name = "lkm_cust_id", referencedColumnName = "cust_id")
        private CustomerEntity customer;
    }
    
  2. 测试

    package test;
    
    
    import cn.App;
    import cn.entity.CustomerEntity;
    import cn.entity.LinkManEntity;
    import cn.repository.CustomerRepository;
    import cn.repository.LinkManRepository;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.annotation.Rollback;
    
    import javax.transaction.Transactional;
    
    @SpringBootTest(classes = App.class)
    public class TestOne2Many {
        @Autowired
        private CustomerRepository customerRepository;
    
        @Autowired
        private LinkManRepository linkManRepository;
    
    
        /**
         * 测试1:保存一个客户,并保存该客户的一个联系人
         * <p>
         * 因为分别调用了customerRepository和linkManRepository的save()方法,故需要加@Transactional保持事务的一致性
         * 而添加@Transactional注解后,在测试类中默认事务是回滚的,虽然测试时添加成功了、但测试程序结束后程序默认回滚导致看不到结果
         * 因此再加一个@Rollback(false),主要是方便人在数据库中查看结果,看是否符合预期
         * <p>
         * 测试环境要求:
         * 1.CustomerEntity实体中使用 @OneToMany 和 @JoinColumn 两个注解进行一对多关系的映射
         * 2.application.yml 中 jpa.hibernate.ddl-auto=create
         */
        @Test
        @Transactional
        @Rollback(false)
        public void testSave1() {
            //创建一个客户,创建一个联系人
            CustomerEntity customer = new CustomerEntity();
            customer.setCustName("百度");
            LinkManEntity linkMan = new LinkManEntity();
            linkMan.setLkmName("小李");
            // 配置客户到联系人的关系(一对多)
            customer.getLinkMans().add(linkMan);
            customerRepository.save(customer);
            linkManRepository.save(linkMan);
            /**
             * 此时控制台SQL(建表语句略)
             * Hibernate: insert into cst_customer (cust_address, cust_industry, cust_level, cust_name, cust_phone, cust_source) values (?, ?, ?, ?, ?, ?)
             * Hibernate: insert into cst_linkman (lkm_cust_id, lkm_email, lkm_gender, lkm_memo, lkm_mobile, lkm_name, lkm_phone, lkm_position) values (?, ?, ?, ?, ?, ?, ?, ?)
             * Hibernate: update cst_linkman set lkm_cust_id=? where lkm_id=?
             *
             * 从客户的角度上:发送两条insert语句,发送一条update语句更新数据库(更新外键)
             * 是由于我们使用@OneToMany配置了客户到联系人的关系,造成了客户也对外键进行维护
             */
        }
    
        /**
         * 测试2:联系人到客户,仅两条insert(与上面相比无update)
         * 这种方式更贴近需求,因为没有多余的update
         * <p>
         * 测试环境要求:
         * 1.CustomerEntity实体中使用 @OneToMany(mappedBy = "customer")
         * 2.application.yml 中 jpa.hibernate.ddl-auto=create
         */
        @Test
        @Transactional
        @Rollback(false)
        public void testSave2() {
            CustomerEntity customer = new CustomerEntity();
            customer.setCustName("腾讯");
            LinkManEntity linkMan = new LinkManEntity();
            linkMan.setLkmName("小王");
            // 配置联系人到客户的关系(多对一)
            linkMan.setCustomer(customer);
            customerRepository.save(customer);
            linkManRepository.save(linkMan);
            /**
             * 此时控制台SQL
             * Hibernate: insert into cst_customer (cust_address, cust_industry, cust_level, cust_name, cust_phone, cust_source) values (?, ?, ?, ?, ?, ?)
             * Hibernate: insert into cst_linkman (lkm_cust_id, lkm_email, lkm_gender, lkm_memo, lkm_mobile, lkm_name, lkm_phone, lkm_position) values (?, ?, ?, ?, ?, ?, ?, ?)
             *
             * 只发送了两条insert语句,由于配置了联系人到客户的映射关系(多对一)
             */
        }
    
        /**
         * 测试3:代码中即配置客户到联系人,又配置联系人到客户
         * <p>
         * 测试环境要求:
         * 1.CustomerEntity实体中使用 @OneToMany 和 @JoinColumn 两个注解进行一对多关系的映射
         * 2.application.yml 中 jpa.hibernate.ddl-auto=create
         * <p>
         * 巨坑出现:java.lang.StackOverflowError !!!
         * 这是因为两个实体类 CustomerEntity 和 LinkManEntity 都使用了 @Data 注解造成的,
         * 最主要的原因是因为 @Data 注解中包含了 @ToString 方法,在 JPA 的框架中,两者互相打印对方造成 StackOverflowError
         * <p>
         * 解决办法:
         * 两个实体类 CustomerEntity 和 LinkManEntity 不要直接使用 @Data 注解
         * 换成 @Getter 和 @Setter 注解,至于需要用到 toString() 方法打印结果时,
         * 手动重写该方法,注意双方一定不要相互包含!!!
         * (用 toString 的感觉也不多、因为查看结果一般是手动进入数据库查看数据的)
         */
        @Test
        @Transactional
        @Rollback(false)
        public void testSave3() {
            CustomerEntity customer = new CustomerEntity();
            customer.setCustName("腾讯");
            LinkManEntity linkMan = new LinkManEntity();
            linkMan.setLkmName("小王");
            // 即配置客户到联系人,又配置联系人到客户
            customer.getLinkMans().add(linkMan);
            linkMan.setCustomer(customer);
            customerRepository.save(customer);
            linkManRepository.save(linkMan);
            /**
             * 此时控制台SQL(也是两条insert,一条update. 同测试1结果)
             * Hibernate: insert into cst_customer (cust_address, cust_industry, cust_level, cust_name, cust_phone, cust_source) values (?, ?, ?, ?, ?, ?)
             * Hibernate: insert into cst_linkman (lkm_cust_id, lkm_email, lkm_gender, lkm_memo, lkm_mobile, lkm_name, lkm_phone, lkm_position) values (?, ?, ?, ?, ?, ?, ?, ?)
             * Hibernate: update cst_linkman set lkm_cust_id=? where lkm_id=?
             */
        }
    
        /**
         * 测试4:Customer放弃外键维护权
         * (代码中也配置客户到联系人,又配置联系人到客户)
         * <p>
         * 测试环境要求:
         * 1.CustomerEntity实体中使用 @OneToMany(mappedBy = "customer")
         * 2.application.yml 中 jpa.hibernate.ddl-auto=create
         */
        @Test
        @Transactional
        @Rollback(false)
        public void testSave4() {
            CustomerEntity customer = new CustomerEntity();
            customer.setCustName("阿里");
            LinkManEntity linkMan = new LinkManEntity();
            linkMan.setLkmName("小赵");
            customer.getLinkMans().add(linkMan);
            linkMan.setCustomer(customer);
            customerRepository.save(customer);
            linkManRepository.save(linkMan);
            /**
             * 此时控制台SQL(同测试2结果)
             * Hibernate: insert into cst_customer (cust_address, cust_industry, cust_level, cust_name, cust_phone, cust_source) values (?, ?, ?, ?, ?, ?)
             * Hibernate: insert into cst_linkman (lkm_cust_id, lkm_email, lkm_gender, lkm_memo, lkm_mobile, lkm_name, lkm_phone, lkm_position) values (?, ?, ?, ?, ?, ?, ?, ?)
             */
        }
    
        /**
         * 由上面的测试结果可知,主要有两大条件
         * 1. "一"的一方是否维护外键 ==> 若是维护则三条SQL、不维护则仅两条SQL
         * 2. 单向/双向绑定
         * <p>
         * 经分析,"一"的一方不维护外键性能会好一些,能达到相同的效果但少一条SQL语句(少一个与数据库的网络交互)
         * 因此剩下 testSave2 和 testSave4,但 testSave4 是双向绑定、代码会稍多
         * 因此 testSave2 是目前能达到效果,SQL最少、代码最少的样例
         * 但此时 testSave2 的单向绑定是联系人到客户,故想再测试下
         * 当"一"的一方放弃维护外键后,客户到联系人的单向绑定是否也能达到相同效果
         * <p>
         * 测试5:联系人到客户,仅两条insert(与上面相比无update)
         * 这种方式更贴近需求,因为没有多余的update
         * <p>
         * 测试环境要求:
         * 1.CustomerEntity实体中使用 @OneToMany(mappedBy = "customer")
         * 2.application.yml 中 jpa.hibernate.ddl-auto=create
         */
        @Test
        @Transactional
        @Rollback(false)
        public void testSave5() {
            CustomerEntity customer = new CustomerEntity();
            customer.setCustName("腾讯");
            LinkManEntity linkMan = new LinkManEntity();
            linkMan.setLkmName("小小王");
    //        // 配置联系人到客户的关系(多对一)
    //        linkMan.setCustomer(customer);
            // 配置客户到联系人的关系(一对多)
            customer.getLinkMans().add(linkMan);
            customerRepository.save(customer);
            linkManRepository.save(linkMan);
            /**
             * 此时控制台SQL(即也只有两条insert)
             * Hibernate: insert into cst_customer (cust_address, cust_industry, cust_level, cust_name, cust_phone, cust_source) values (?, ?, ?, ?, ?, ?)
             * Hibernate: insert into cst_linkman (lkm_cust_id, lkm_email, lkm_gender, lkm_memo, lkm_mobile, lkm_name, lkm_phone, lkm_position) values (?, ?, ?, ?, ?, ?, ?, ?)
             *
             * 虽然此时只有两条SQL,但观察数据库中的数据发现,cst_linkman 联系人表中,外键 lkm_cust_id 为 null,即表示根本没有维护外键!!!
             * 因此该方案没有达到预期效果,直接pass
             *
             * 最终结论:
             * "一"的一方放弃维护外键、单向绑定(多对一绑定),这种方案发送的SQL最少、代码量最少,即样例 testSave2()
             * (单向绑定只能是多对一、即配置联系人到客户的关系;
             * 而不能是一对多、配置客户到联系人的关系,这样会造成外键根本没有插入)
             */
        }
    }
    
  • 分析:由上面测试结果可以看出,主要由两大条件,第一是 "一"的一方是否维护外键关系,即"一"的一方是否配置了 @JoinColumn 注解;第二是 代码中的单向/双向绑定(即配置客户到联系人、又配置联系人到客户)。测试结果总结如下

    序号条件结果
    测试一:testSave1"一"的一方维护外键
    单向绑定
    两个 insert、一个update (用于维护外键)
    测试二:testSave2"一"的一方不维护外键
    单向绑定(联系人到客户,多对一)
    仅两个 insert
    测试三:testSave3"一"的一方维护外键
    双向绑定
    两个 insert、一个update (用于维护外键)
    测试四:testSave4"一"的一方不维护外键
    双向绑定
    仅两个 insert
    测试五:testSave5"一"的一方不维护外键
    单向绑定(客户到联系人,一对多)
    仅两个 insert、但此时外键没插入数据,
    功能都没达到效果,直接pass!
    • 结果:由 testSave5() 注解说明可知:"一"的一方放弃维护外键、单向绑定(多对一绑定),这种方案发送的SQL最少、代码量最少,即样例 testSave2() 代码。特别说明,单向绑定只能是多对一、即配置联系人到客户的关系;而不能是一对多、配置客户到联系人的关系,这样会造成外键根本没有插入。

2.2 级联

概述

  1. 多表关系的删除
    1. "多"的一方(即从表)可以随意删除数据,而"一"的一方(即主表)主键有可能被引用、即存在外键约束,不能随意删除数据。
    2. 样例:客户与联系人中,"多"的一方联系人表可以随意删除数据,而"一"的一方客户表不能随意删数据。
  2. 级联概述
    1. 含义:操作一个对象的时候也操作它的关联对象
    2. 分类:级联添加、级联删除、级联更新
    3. 样例:级联添加即保存一个客户的同时保存联系人;级联删除即删除一个客户的同时也删除他的所有联系人
  3. 级联操作
    1. 需要区分操作主题
    2. 需要在操作主题的实体类上,添加级联属性
    3. 配置级联:可以配置到设置多表的映射关系的注解上。PERSIST (保存)、MERGE (更新)、REMOVE (删除)、ALL (包含增删改),推荐配置成 ALL
    4. 样例:@OneToMany(mappedBy = "customer",cascade = CascadeType.ALL)
  4. Date JPA 的删除
    1. 删除从表数据:可以随时任意删除
    2. 删除主表数据:【1】 有从表数据引用。(a) 在默认情况下,它会把外键字段置为null,然后删除主表数据。如果在数据库的表结构上,外键字段有非空约束,默认情况就会报错了;(b) 如果配置了放弃维护关联关系的权利,则不能删除(与外键字段是否允许为null,没有关系)因为在删除时,它根本不会去更新从表的外键字段了;© 如果还想删除,使用级联删除引用。(注:实际开发中级联删除慎用!)【2】没有从表数据引用:随便删

编码

  1. 修改实体类

    • 即在客户实体中添加个 cascade 属性,变为 @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL) ,其他代码不动。
    package cn.entity;
    
    
    import lombok.Getter;
    import lombok.Setter;
    
    import javax.persistence.*;
    import java.util.HashSet;
    import java.util.Set;
    
    @Entity
    @Table(name = "cst_customer")
    //@Data
    @Getter
    @Setter
    public class CustomerEntity {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @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")
        @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
        private Set<LinkManEntity> linkMans = new HashSet<LinkManEntity>();
    }
    
    
    package cn.entity;
    
    import lombok.Getter;
    import lombok.Setter;
    
    import javax.persistence.*;
    
    @Entity
    @Table(name = "cst_linkman")
    //@Data
    @Getter
    @Setter
    public class LinkManEntity {
        @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; // 联系人备注
    
        @ManyToOne(targetEntity = CustomerEntity.class)
        @JoinColumn(name = "lkm_cust_id", referencedColumnName = "cust_id")
        private CustomerEntity customer;
    }
    
  2. 测试

    package test;
    
    import cn.App;
    import cn.entity.CustomerEntity;
    import cn.entity.LinkManEntity;
    import cn.repository.CustomerRepository;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.annotation.Rollback;
    
    import javax.transaction.Transactional;
    
    @SpringBootTest(classes = App.class)
    public class TestOne2ManyCascade {
        @Autowired
        private CustomerRepository customerRepository;
    
        /**
         * 测试级联添加,即保存一个客户的同时、保存他的所有联系人
         * <p>
         * 测试环境要求:
         * 1.CustomerEntity实体中使用 @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
         * 2.application.yml 中 jpa.hibernate.ddl-auto=create
         */
        @Test
        @Transactional
        @Rollback(false)
        public void testCascadeAdd() {
            // 模拟一个客户和两个联系人
            CustomerEntity customer = new CustomerEntity();
            customer.setCustName("美团");
            LinkManEntity linkMan1 = new LinkManEntity();
            linkMan1.setLkmName("小明");
            LinkManEntity linkMan2 = new LinkManEntity();
            linkMan2.setLkmName("小强");
            // 级联时必需双向配置关系!
            linkMan1.setCustomer(customer);
            customer.getLinkMans().add(linkMan1);
            linkMan2.setCustomer(customer);
            customer.getLinkMans().add(linkMan2);
            // 仅保存客户
            customerRepository.save(customer);
            /**
             * 控制台SQL
             * Hibernate: insert into cst_customer (cust_address, cust_industry, cust_level, cust_name, cust_phone, cust_source) values (?, ?, ?, ?, ?, ?)
             * Hibernate: insert into cst_linkman (lkm_cust_id, lkm_email, lkm_gender, lkm_memo, lkm_mobile, lkm_name, lkm_phone, lkm_position) values (?, ?, ?, ?, ?, ?, ?, ?)
             * Hibernate: insert into cst_linkman (lkm_cust_id, lkm_email, lkm_gender, lkm_memo, lkm_mobile, lkm_name, lkm_phone, lkm_position) values (?, ?, ?, ?, ?, ?, ?, ?)
             * 即把该客户和他的联系人均已添加
             */
        }
    
        /**
         * 测试级联删除,即删除一个客户的同时、删除他的所有联系人
         * <p>
         * 测试环境要求:
         * 1.CustomerEntity实体中使用 @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
         * 2.application.yml 中 jpa.hibernate.ddl-auto=update
         * 因为若是create时重新建表数据库中没有数据;可以先测试下上面的级联添加,有数据后改为update,然后测试级联删除
         */
        @Test
        @Transactional
        @Rollback(false)
        public void testCascadeRemove() {
            // 步骤:先查询1号客户,再删除(且删除他的所有联系人)
            CustomerEntity customer = customerRepository.getById(1L);
            customerRepository.delete(customer);
            /**
             * 控制台SQL
             * Hibernate: select customeren0_.cust_id as cust_id1_0_0_, customeren0_.cust_address as cust_add2_0_0_, customeren0_.cust_industry as cust_ind3_0_0_, customeren0_.cust_level as cust_lev4_0_0_, customeren0_.cust_name as cust_nam5_0_0_, customeren0_.cust_phone as cust_pho6_0_0_, customeren0_.cust_source as cust_sou7_0_0_ from cst_customer customeren0_ where customeren0_.cust_id=?
             * Hibernate: select linkmans0_.lkm_cust_id as lkm_cust9_1_0_, linkmans0_.lkm_id as lkm_id1_1_0_, linkmans0_.lkm_id as lkm_id1_1_1_, linkmans0_.lkm_cust_id as lkm_cust9_1_1_, linkmans0_.lkm_email as lkm_emai2_1_1_, linkmans0_.lkm_gender as lkm_gend3_1_1_, linkmans0_.lkm_memo as lkm_memo4_1_1_, linkmans0_.lkm_mobile as lkm_mobi5_1_1_, linkmans0_.lkm_name as lkm_name6_1_1_, linkmans0_.lkm_phone as lkm_phon7_1_1_, linkmans0_.lkm_position as lkm_posi8_1_1_ from cst_linkman linkmans0_ where linkmans0_.lkm_cust_id=?
             * Hibernate: delete from cst_linkman where lkm_id=?
             * Hibernate: delete from cst_linkman where lkm_id=?
             * Hibernate: delete from cst_customer where cust_id=?
             *
             * 即会先查询客户表和联系人表,然后把该客户和他的所有联系人都删除
             */
        }
    }
    

链接

  1. 源码链接:blogs-jpa
  2. 关联映射多对多链接:JPA四:关联映射(二)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值