SpringDataJPA

JPA 第三弹

回顾:

    springDataJPA的运行过程和原理:

1. 通过 `org.springframework.aop.framework.JdkDynamicAopProxy`的`invoke`方法创建了一个动态代理对象
2. `org.springframework.data.jpa.repository.support.SimpleJpaRepository`中封装了 JPA 的操作(借助 JPA 的 API 完成数据库的 CRUD 操作)
3. 由于 JPA 是一套规范,不会单独的操作数据库,所以需要提供 JPA 规范的实现。所以需要通过 Hibernate 来实现数据库的操作

   

   springDataJpa、jpa规范、Hibernate三者之间的关系:

    我们的代码要符合 springDatajpa  规范,springDataJpa 是对 Jpa 规范的扩展,最底层实现(操作数据库的)是Hibernate



    符合springDataJpa规范的dao层接口的编写规则:

        1. 需要实现两个接口(`org.springframework.data.jpa.repository.JpaRepository`,`org.springframework.data.jpa.repository.JapSpecificationExecutor`)

        2. 提供响应的泛型  

   

    运行过程:

         动态代理的方式:动态代理对象 



   普通 CRUD 操作

1 Specifications 动态查询

有时我们在查询某个实体的时候,给定的条件是不固定的,这时就需要动态构建相应的查询语句,在 Spring Data JPA 中可以通过 org.springframework.data.jpa.repository.JpaSpecificationExecutor 接口查询。相比JPQL,其优势是类型安全,更加的面向对象。

package org.springframework.data.jpa.repository;

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;

// JpaSpecificationExecutor中定义的方法
public interface JpaSpecificationExecutor<T> {

    // 根据条件查询一个对象
    T findOne(Specification<T> spec);

    // 根据条件查询集合
    List<T> findAll(Specification<T> spec);

    // 根据条件分页查询
    Page<T> findAll(Specification<T> spec, Pageable pageable);

    // 排序查询查询
    List<T> findAll(Specification<T> spec, Sort sort);

    // 统计查询
    long count(Specification<T> spec);
}

对于JpaSpecificationExecutor,这个接口基本是围绕着 org.springframework.data.jpa.domain.Specification 接口来定义的。我们可以简单的理解为,Specification 构造的就是查询条件。

Specification 接口中只定义了如下一个方法:

public interface Specification<T> {

    /**
     * 构造查询条件
     * 
     * @param root Root接口,代表查询的根对象,可以通过root获取实体中的属性
     * @param query 代表一个顶层查询对象,用来自定义查询
     * @param cb 用来构建查询,此对象里有很多条件方法
     */
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}

1.1 使用Specifications完成条件查询

package com.itlaobing.test;

import com.itlaobing.dao.CustomerDao;
import com.itlaobing.entity.Customer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-application.xml")
public class SpecificationTest {


    @Autowired
    private CustomerDao customerDao;

    /**
     * 精确匹配, 匹配 custName 为 云创动力 的客户信息
     */
    @Test
    public void testSpecification() {
        //使用匿名内部类的方式,创建一个Specification的实现类,并实现toPredicate方法
        Specification<Customer> spec = new Specification<Customer>() {
            //cb:构建查询,添加查询方式   equal:精准匹配
            //root:从实体Customer对象中按照custName属性进行查询
            @Override
            public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                // 1. 获取比较属性
                Path<Object> custName = root.get("custName");
                // 2. 构建查询条件
                Predicate predicate = cb.equal(custName, "云创动力");
                return predicate;
            }
        };
        Customer customer = customerDao.findOne(spec);
        System.out.println(customer);
    }

    /**
     * 多条件匹配, 匹配客户名称为 云创动力 且 地址为 西安市未央区的
     */
    @Test
    public void testSpecification1() {
        Specification<Customer> specification = new Specification<Customer>() {
            @Override
            public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Path<Object> custName = root.get("custName"); //客户名称
                Path<Object> address = root.get("custAddress"); //地址

                // 构建客户名匹配
                Predicate predicate1 = cb.equal(custName, "云创动力");
                // 构建地址匹配
                Predicate predicate2 = cb.equal(address, "西安市未央区");
                // 将多个条件组合, 两者都要满足 与关系, 满足其一即可 或关系

                // 与关系
                Predicate result = cb.and(predicate1, predicate2);
                // 或关系
//                Predicate result = cb.or(predicate1, predicate2);
                return result;
            }
        };
        List<Customer> lists = customerDao.findAll(specification);
        System.out.println(lists);
    }

    /**
     * 模糊匹配, 匹配 custName 中包含 云创 的客户信息
     */
    @Test
    public void testSpecification2(){
        Specification<Customer> spec = new Specification<Customer>() {
            @Override
            public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                // 查询属性:客户名称
                Path<Object> custName = root.get("custName");
                // 查询方式,模糊查询 like
                /**
                 * equal: 可以直接使用 Path 对象
                 * 其他的方法得到 Path 对象后,需要指定数据类型.
                 * 指定的方式是 path.as(类型的字节码对象)
                 */
                Predicate predicate = cb.like(custName.as(String.class), "%云创%");
                return predicate;
            }
        };
        List<Customer> lists = customerDao.findAll(spec);
        for (Customer list : lists) {
            System.out.println(list);
        }
    }
}

1.2 基于Specifications的排序

使用的方法是 JpaSpecificationExecutor 接口的中的

List<T> findAll(Specification<T> spec, Sort sort);方法

这里面需要构造一个 org.springframework.data.domain.Sort 对象, Sort对象的构造方法有多个

我们使用public Sort(Direction direction, String... properties)

  • 第一个参数: 排序的顺序 (升序、倒序)
    • Sort.Direction.ASC 升序
    • Sort.Direction.DESC 降序
  • 排序的属性名称
/**
     * 排序
     */
    @Test
    public void testSpecification3(){
        Specification<Customer> spec = new Specification<Customer>() {
            @Override
            public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                // 查询属性:客户名称
                Path<Object> custName = root.get("custName");
                // 查询方式,模糊查询 like
                /**
                 * equal: 可以直接使用 Path 对象
                 * 其他的方法得到 Path 对象后,需要指定数据类型.
                 * 指定的方式是 path.as(类型的字节码对象)
                 */
                Predicate predicate = cb.like(custName.as(String.class), "%云创%");
                return predicate;
            }
        };
        /**
         * 排序, 需要构造 Sort 对象。
         * sort对象的构造方法有多个,我们使用
         *  public Sort(Direction direction, String... properties)
         *    第一个参数: 排序的顺序 (升序、倒序)
         *          Sort.Direction.ASC  升序
         *          Sort.Direction.DESC 降序
         *    第二个参数: 排序的属性名称
         */

        Sort sort = new Sort(Sort.Direction.DESC, "custId");
        List<Customer> lists = customerDao.findAll(spec, sort);
        for (Customer list : lists) {
            System.out.println(list);
        }
    }

1.3 基于Specifications的分页查询

使用的方法是 JpaSpecificationExecutor 接口的中的

Page<T> findAll(Specification<T> spec, Pageable pageable)方法

需要用到 org.springframework.data.domain.Pageable 接口,PageRequest 类实现了 Pageable 接口,调用构造方法的形式构造

public PageRequest(int page, int size):

  • 第一个参数: 页码(从0开始)
  • 第二个参数: 每页查询条数
  • 返回一个 org.springframework.data.domain.Page 对象,这是SpringDataJPA为我们封装好的 PageBean 对象
@Test
    public void testPage() {
        //构造查询条件
        Specification<Customer> spec = new Specification<Customer>() {
            public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return cb.like(root.get("custName").as(String.class), "%云创%");
            }
        };
        /**
         * 构造分页参数
         *      Pageable : 接口
         *          PageRequest实现了Pageable接口,调用构造方法的形式构造
         *              第一个参数:页码(从0开始)
         *              第二个参数:每页查询条数
         */
        Pageable pageable = new PageRequest(1, 5);

        /**
         * 分页查询,封装为Spring Data Jpa 内部的page bean
         *      此重载的findAll方法为分页方法需要两个参数
         *          第一个参数:查询条件Specification
         *          第二个参数:分页参数
         */
        Page<Customer> page = customerDao.findAll(spec, pageable);
        // 获取内容
        List<Customer> lists = page.getContent();
        System.out.println(lists);
    }

对于Spring Data JPA中的分页查询,是其内部自动实现的封装过程,返回的是一个Spring Data JPA提供的 pageBean 对象。其中的方法说明如下:

//获取总页数
int getTotalPages();
//获取总记录数    
long getTotalElements();
//获取列表数据
List<T> getContent();

1.4 CriteriaQuery 方法对应关系

方法名称 Sql对应关系
equle filed = value
gt(greaterThan ) filed > value
lt(lessThan ) filed < value
ge(greaterThanOrEqualTo ) filed >= value
le( lessThanOrEqualTo) filed <= value
notEqule filed != value
like filed like value
notLike filed not like value

第2章 多表设计

2.1 表之间关系的划分

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

从图可以看出,系统设计的三种实体关系分别为:

  • 多对多
    • 一般采用中间表的方式实现
    • 中间表一般至少由两个字段组成,这两个字段作为外键指向两张表的主键,这两个字段组成联合主键
  • 一对多
    • 一的一方叫做 主表
    • 多的一方叫做 从表
    • 外键: 在 从表 中新建一列作为外键,它的取值来源于主表的主键
  • 一对一

注意:一对多关系可以看为两种: 即一对多,多对一。所以说四种更精确。

明确: 我们今天只涉及实际开发中常用的关联关系,一对多和多对多。而一对一的情况,在实际开发中几乎不用。

2.2 在JPA框架中表关系的分析步骤

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

实体间的关系:

  • 包含关系:通过这种关系描述表关系
  • 继承关系

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

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

第二步:在数据库中实现两张表的关系

通过 外键 | 中间表 描述

第三步:在实体类中描述出两个实体的关系

实体类中使用包含关系描述

第四步:配置出实体类和数据库表的关系映射(重点)

第3章 JPA中的一对多

3.1 示例分析

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

客户: 指买我们公司产品的公司

联系人: 指公司的员工

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

3.2 表关系建立

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

什么是外键?

指的是从表中有一列,取值参照主表的主键,这一列就是外键。

一对多数据库关系的建立,如下图所示

3.3 实体类关系建立以及映射配置

在实体类中,由于客户是少的一方,它应该包含多个联系人,所以实体类要体现出客户中有多个联系人的信息,代码如下:

package com.itlaobing.entity;

import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

/**
 * @Classname Customer
 *     所有的注解都是使用JPA的规范提供的注解,
 *     所以在导入注解包的时候,一定要导入javax.persistence下的
 * @Date 2020/4/9 10:10
 * @Author by Administrator
 * @Version v1.0
 */
//@Data
@Entity //声明实体类
@Table(name="cust_customer") //建立实体类和表的映射关系
public class Customer implements Serializable {

//    @Id  //声明当前私有属性为主键
//    @GeneratedValue(strategy= GenerationType.IDENTITY) //配置主键的生成策略
//    @Column(name="cust_id") //指定和表中cust_id字段的映射关系
//    private Long custId;

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator="payablemoney_gen")
    @TableGenerator(name = "payablemoney_gen",
            table="tb_generator",
            pkColumnName="gen_name",
            valueColumnName="gen_value",
            pkColumnValue="CUSTOMER_PK",
            allocationSize=1
    )
    private Long custId;



    @Column(name="cust_name") //指定和表中cust_name字段的映射关系
    private String custName;

    @Column(name="cust_source")//指定和表中cust_source字段的映射关系
    private String custSource;

    @Column(name="cust_industry")//指定和表中cust_industry字段的映射关系
    private String custIndustry;

    @Column(name="cust_level")//指定和表中cust_level字段的映射关系
    private String custLevel;

    @Column(name="cust_address")//指定和表中cust_address字段的映射关系
    private String custAddress;

    @Column(name="cust_phone")//指定和表中cust_phone字段的映射关系
    private String custPhone;


    /**
     * 配置 一对多 关系
     * 使用注解配置多表关系
     *   1. 声明关系
     *      @oneToMany: 配置一对多关系
     *        targetEntity; 对方对象的字节码
     *   2. 配置外键(中间表)
     *      @JoinColumn: 配置外键
     *         name: 外键字段名称
     *         referencedColumnName: 参照的主表的主键名称
     *   在客户实体(一的一方)上面配置了外键,所以客户也具备了维护外键的作用
     */
    @OneToMany(targetEntity = Contacts.class)
    @JoinColumn(name = "cust_id", referencedColumnName = "custId")
    private Set<Contacts> contactsSet = new HashSet<Contacts>();

    // 省略 getter/setter 方法
}

由于联系人是多的一方,在实体类中要体现出,每个联系人只能对应一个客户,代码如下:

package com.itlaobing.entity;

import javax.persistence.*;

/**
 * @Classname Contacts
 * @Description TODO()
 * @Date 2020/4/24 16:46
 * @Author by Administrator
 * @Version v1.0
 */
//@Data
@Entity
@Table(name = "cust_contacts")
public class Contacts {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "contacts_id")
    //联系人编号
    private int contactsId;

    @Column(name="contacts_name")
    //联系人姓名
    private String contactsName;

    @Column(name="contacts_gender")
    //联系人性别
    private String contactsGender;

    @Column(name="contacts_phone")
    // 联系人电话
    private String contactsPhone;

    @Column(name="contacts_email")
    //联系人邮箱
    private String contactsEmail;

    @Column(name="contacts_memo")
    //联系人备注
    private String contactsMemo;

    /**
     * 配置联系人到客户的 多对一 关系
     * 使用注解配置 多对一 关系
     *    1. 配置表关系
     *    2. 配置外键(中间表)
     *  多个一方配置外键,那么多的一方也会维护外键
     */
    @ManyToOne(targetEntity = Customer.class)
    @JoinColumn(name = "cust_id", referencedColumnName = "custId")
    private Customer customer;
    
    //省略 getter/setter 方法
}

现在客户实体中能找到联系人,联系人实体中能找到客户。这种关系我们叫做双向关系。如果只配置任意一方,表示单向关系

3.4 映射的注解说明

@OneToMany:

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

​ 属性:

​ targetEntity:指定多的多方的类的字节码

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

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

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

​ orphanRemoval:是否使用孤儿删除

@ManyToOne

​ 作用:建立多对一的关系

​ 属性:

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

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

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

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

@JoinColumn

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

​ 属性:

​ name:指定外键字段的名称

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

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

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

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

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

​ columnDefinition:列的定义信息。

3.5 一对多的操作

在测试前,可以在spring-application.xml文件中添加一个配置,以便更好的观察

<!-- jpa hibernate 配置
        hibernate.hbm2ddl.auto:
            create 每次启动都重新创建表
            update 有表不会创建,没表创建
-->
<property name="jpaProperties">
    <props>
        <prop key="hibernate.hbm2ddl.auto">create</prop>
    </props>
</property>

3.5.1 添加

package com.itlaobing;

import com.itlaobing.dao.ContactsDao;
import com.itlaobing.dao.CustomerDao;
import com.itlaobing.entity.Contacts;
import com.itlaobing.entity.Customer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

/**
 * @Classname test
 * @Description TODO()
 * @Date 2020/4/24 17:18
 * @Author by Administrator
 * @Version v1.0
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-application.xml")
public class OneToManyTest {

    @Autowired
    private CustomerDao customerDao;

    @Autowired
    private ContactsDao contactsDao;

    /**
     * 保存一个客户,保存一个联系人
     *  结果: 客户和联系人独立的保存在数据库表中
     *          联系人外键为空
     *    原因:实体类中没有使用配置关系,所以我们需要在代码中加入
     *      customer.getContactsSet().add(contacts);
     *     或者 contacts.setCustomer(customer);
     */
    @Test
    @Transactional //配置事务
    @Rollback(false) // 不自动回滚
    public void testAdd(){
        // 创建一个客户,创建一个联系人
        Customer customer = new Customer();
        customer.setCustName("阿里巴巴");

        Contacts contacts = new Contacts();
        contacts.setContactsName("小马");

//        customer.getContactsSet().add(contacts);
//        contacts.setCustomer(customer);

        // 保存
        customerDao.save(customer);
        contactsDao.save(contacts);
    }
}

通过保存的案例,我们可以发现在设置了双向关系之后,使用customer.getContactsSet().add(contacts)会发送两条insert语句,一条多余的update语句。所以我们的解决方法就是一的一方放弃维护权

修改后的内容为:

package com.itlaobing.entity;

import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

/**
 * @Classname Customer
 *     所有的注解都是使用JPA的规范提供的注解,
 *     所以在导入注解包的时候,一定要导入javax.persistence下的
 * @Date 2020/4/9 10:10
 * @Author by Administrator
 * @Version v1.0
 */
@Data
@Entity //声明实体类
@Table(name="cust_customer") //建立实体类和表的映射关系
public class Customer implements Serializable {

//    @Id  //声明当前私有属性为主键
//    @GeneratedValue(strategy= GenerationType.IDENTITY) //配置主键的生成策略
//    @Column(name="cust_id") //指定和表中cust_id字段的映射关系
//    private Long custId;

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator="payablemoney_gen")
    @TableGenerator(name = "payablemoney_gen",
            table="tb_generator",
            pkColumnName="gen_name",
            valueColumnName="gen_value",
            pkColumnValue="CUSTOMER_PK",
            allocationSize=1
    )
    private Long custId;



    @Column(name="cust_name") //指定和表中cust_name字段的映射关系
    private String custName;

    @Column(name="cust_source")//指定和表中cust_source字段的映射关系
    private String custSource;

    @Column(name="cust_industry")//指定和表中cust_industry字段的映射关系
    private String custIndustry;

    @Column(name="cust_level")//指定和表中cust_level字段的映射关系
    private String custLevel;

    @Column(name="cust_address")//指定和表中cust_address字段的映射关系
    private String custAddress;

    @Column(name="cust_phone")//指定和表中cust_phone字段的映射关系
    private String custPhone;


    /**
     * 配置 一对多 关系
     * 使用注解配置多表关系
     *   1. 声明关系
     *      @oneToMany: 配置一对多关系
     *        targetEntity; 对方对象的字节码
     *   2. 配置外键(中间表)
     *      @JoinColumn: 配置外键
     *         name: 外键字段名称
     *         referencedColumnName: 参照的主表的主键名称
     *   在客户实体(一的一方)上面配置了外键,所以客户也具备了维护外键的作用
     */
//    @OneToMany(targetEntity = Contacts.class)
//    @JoinColumn(name = "cust_id", referencedColumnName = "custId")
    /**
     * 放弃外键维护权,只需要声明关系即可
     *      mappedBy:对方配置关系的属性名称(即Contacts类中的 customer)
     */
    @OneToMany(mappedBy = "customer")
    private Set<Contacts> contactsSet = new HashSet<Contacts>();

}

修改后就只能通过使用contacts.setCustomer(customer)来维护外键了。

3.5.2 删除

删除操作的说明如下:

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

删除主表数据:

  • 有从表数据

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

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

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

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

/**
 *  删除
 */
@Test
@Transactional
@Rollback(false)
public void deleteTest(){
    Customer customer = new Customer();
    customer.setCustName("阿里巴巴");
    // 保存
    customerDao.save(customer);

    //删除
    customerDao.delete(1L);
}

在实际开发中,级联删除请慎用!(在一对多的情况下)

3.5.3 级联操作

级联操作:

  • 指操作一个对象同时操作它的关联对象
  • 需要区分操作主题
  • 需要在操作主体的实体类上添加级联属性(添加到多表映射关系的注解上)
  • 配置cascade

级联操作有以下几种:

  • 级联添加:
    • 当保存客户的同时保存联系人
  • 级联删除:
    • 当删除客户的同时删除所有联系人
@Entity //声明实体类
@Table(name="cust_customer") //建立实体类和表的映射关系
public class Customer implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator="payablemoney_gen")
    @TableGenerator(name = "payablemoney_gen",
            table="tb_generator",
            pkColumnName="gen_name",
            valueColumnName="gen_value",
            pkColumnValue="CUSTOMER_PK",
            allocationSize=1
    )
    private Long custId;


    @Column(name="cust_name") //指定和表中cust_name字段的映射关系
    private String custName;

    @Column(name="cust_source")//指定和表中cust_source字段的映射关系
    private String custSource;

    @Column(name="cust_industry")//指定和表中cust_industry字段的映射关系
    private String custIndustry;

    @Column(name="cust_level")//指定和表中cust_level字段的映射关系
    private String custLevel;

    @Column(name="cust_address")//指定和表中cust_address字段的映射关系
    private String custAddress;

    @Column(name="cust_phone")//指定和表中cust_phone字段的映射关系
    private String custPhone;


    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
    private Set<Contacts> contactsSet = new HashSet<Contacts>();
    
    // 省略 getter/setter 方法
}
    /**
     *  级联添加: 保存一个客户的同时,保存联系人
     *    需要在操作主体的实体类上配置cascade属性
     *    我们配置了 cascade = CascadeType.ALL
     */
    @Test
    @Transactional
    @Rollback(false)
    public void testCascadeAdd(){
        Customer customer = new Customer();
        customer.setCustName("阿里巴巴");

        Contacts contacts = new Contacts();
        contacts.setContactsName("小马");

        contacts.setCustomer(customer);

        customer.getContactsSet().add(contacts);

        // 保存
        customerDao.save(customer);
    }

    /**
     * 级联删除
     *   删除一号客户的同时删除一号客户的所有联系人
     * 注:将 spring-application.xml 中
     *    hibernate.hbm2ddl.auto 的值改为 update
     */
    @Test
    @Transactional
    @Rollback(false)
    public void testCascadeDelete(){
        // 两种方式都可以
        customerDao.delete(1L);

//        // 查询客户信息
//        Customer customer = customerDao.findOne(1L);
//        // 删除
//        customerDao.delete(customer);
    }

cascade:配置级联属性

  • CascadeType.ALL 所有 (推荐)

  • CascadeType.MERGE 更新

  • CascadeType.PERSIST 保存

  • CascadeType.REMOVE 删除

第4章 JPA中的多对多

4.1 示例分析

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

  • 用户:指的是班上的每一个同学。

  • 角色:指的是班上同学的身份信息。

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

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

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

步骤:

  1. 明确表关系 - 多对多关系
  2. 确定表关系 - 中间表
  3. 编写实体类
    1. 用户: 包含角色的集合
    2. 角色: 包含用户的集合
  4. 配置映射关系

4.2 表关系建立

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

4.3 实体类关系建立以及映射配置

一个用户可以具有多个角色,所以在用户实体类中应该包含多个角色的信息,代码如下:

package com.itlaobing.entity;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

/**
 * @Classname SysUser
 * @Description TODO()
 * @Date 2020/4/26 15:27
 * @Author by Administrator
 * @Version v1.0
 */
@Entity
@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 int age;

    /**
     * 配置用户到角色多对多关系
     *   配置多对多关系:
     *    1. 声明表关系
     *      targetEntity: 对方的实体类字节码
     *    2. 配置中间表(包含两个外键)
     *       @JoinTable:
     *          name: 中间表的名称
     *          joinColumns:当前对象在中间表中的外键
     *              @JoinColumn:
     *                  name: 中间表外键名
     *                  referencedColumnName: 主表主键名
     *          inverseJoinColumns:对方对象在中间表的外键
     *              @JoinColumn:
     *                  name: 中间表外键名
     *                  referencedColumnName: 主表主键名
     */
    @ManyToMany(targetEntity = Role.class)
    @JoinTable(name = "sys_user_role",
            // joinColumns 当前对象在中间表中的外键
            joinColumns = {@JoinColumn(name = "sys_user_id", referencedColumnName = "user_id")},
            // inverseJoinColumns 对方对象在中间表的外键
            inverseJoinColumns = {@JoinColumn(name = "sys_role_id", referencedColumnName = "role_id")}
    )
    private Set<Role> roles = new HashSet<>();

   // 省略 getter/setter 方法
}

一个角色可以赋予多个用户,所以在角色实体类中应该包含多个用户的信息,代码如下:

package com.itlaobing.entity;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

/**
 * @Classname Role
 * @Description TODO()
 * @Date 2020/4/26 15:32
 * @Author by Administrator
 * @Version v1.0
 */
@Entity
@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)
    @JoinTable(name = "sys_user_role",
            // joinColumns 当前对象在中间表中的外键
            joinColumns = {@JoinColumn(name = "sys_role_id", referencedColumnName = "role_id")},
            // inverseJoinColumns 对方对象在中间表的外键
            inverseJoinColumns = {@JoinColumn(name = "sys_user_id", referencedColumnName = "user_id")}
    )
    private Set<User> users = new HashSet<>();

    // 省略 getter/setter 方法
}

4.4 映射的注解说明

@ManyToMany

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

​ 属性:

​ cascade:配置级联操作。

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

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

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

@JoinTable

​ 作用:针对中间表的配置

​ 属性:

​ name:配置中间表的名称

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

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

@JoinColumn

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

​ 属性:

​ name:指定外键字段的名称

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

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

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

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

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

​ columnDefinition:列的定义信息。

4.5 多对多的操作

4.5.1 保存

package com.itlaobing.test;

import com.itlaobing.dao.RoleDao;
import com.itlaobing.dao.UserDao;
import com.itlaobing.entity.Role;
import com.itlaobing.entity.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

/**
 * @Classname ManyToManyTest
 * @Description TODO()
 * @Date 2020/4/26 16:00
 * @Author by Administrator
 * @Version v1.0
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-application.xml")
public class ManyToManyTest {

    @Autowired
    private UserDao userDao;

    @Autowired
    private RoleDao roleDao;

    /**
     * 保存用户和角色
     */
    @Test
    @Transactional
    @Rollback(false)
    public void testAdd(){
        // 创建用户
        User user = new User();
        user.setUserName("后羿");
        // 创建角色
        Role role = new Role();
        role.setRoleName("射手");

        /**
         * 如果同时使用两个配置代码,会因为两者都对中间表添加 1 - 1 而报错(主键冲突)
         *  解决方式就是其中一方放弃维护权
         *  多对多中放弃维护权:被动的一方放弃维护权
         */
        // 配置用户到角色的关联关系,可以对中间表数据进行维护 1 - 1
        user.getRoles().add(role);
        // 配置角色到用户的关联关系,可以对中间表数据进行维护 1 - 1
        role.getUsers().add(user);

        // 保存
        userDao.save(user);
        roleDao.save(role);
    }
}

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

(Role.java)
 /**
  * 放弃维护中间表数据权
  *    mappedBy: 对方实体中对象的名称
  */
@ManyToMany(mappedBy = "roles")
private Set<User> users = new HashSet<>();

4.5.2 删除

spring-application.xml配置文件做出如下修改

<property name="jpaProperties">
    <props>
        <prop key="hibernate.hbm2ddl.auto">update</prop>
    </props>
</property>
/**
 * 删除操作
 *  在多对多的删除时,双向级联删除根本不能配置
 * 禁用
 *  如果配了的话,如果数据之间有相互引用关系,可能会清空所有数据
 */
@Test
@Transactional
@Rollback(false)//设置为不回滚
public void testDelete() {
    // 会删除 sys_user 表中的相应数以及 sys_user_role 表中相关数据
    userDao.delete(1l);
}

4.5.3 级联操作

在操作主体(User.java)实体中,配置多表关系中添加 cascade 配置。如下:

User.java

@ManyToMany(targetEntity = Role.class, cascade = CascadeType.ALL)
    @JoinTable(name = "sys_user_role",
            // joinColumns 当前对象在中间表中的外键
            joinColumns = {@JoinColumn(name = "sys_user_id", referencedColumnName = "user_id")},
            // inverseJoinColumns 对方对象在中间表的外键
            inverseJoinColumns = {@JoinColumn(name = "sys_role_id", referencedColumnName = "role_id")}
    )
    private Set<Role> roles = new HashSet<>();
/**
 * 级联添加
 *   保存一个用户的同时,保存用户的关联角色
 */
@Test
@Transactional
@Rollback(false)
public void testCascadeAdd(){
    // 创建用户
    User user = new User();
    user.setUserName("后羿");
    // 创建角色
    Role role = new Role();
    role.setRoleName("射手");

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

    // 保存
    userDao.save(user);
}

/**
 * 级联删除
 *   删除ID为1的用户,同时删除它的关联对象
 */
@Test
@Transactional
@Rollback(false)
public void testCascadeRemove(){
    // 删除
    userDao.delete(1L);

    // 或者使用
//    User user = userDao.findOne(1L);
//    userDao.delete(user);
}

第5章 Spring Data JPA中的多表查询

5.1 对象导航查询

对象图导航检索方式是根据已经加载的对象,导航到他的关联对象。它利用类与类之间的关系来检索对象。例如:我们通过ID查询方式查出一个客户,可以调用Customer类中的getContactsSet()方法来获取该客户的所有联系人。对象导航查询的使用要求是:两个对象之间必须存在关联关系。

查询一个客户,获取该客户下的所有联系人

@Test
@Transactional // 解决 no session 问题
public void findOneTest(){
    Customer customer = customerDao.findOne(1L);
    // 对象导航查询
    Set<Contacts> contactSet = customer.getContactsSet();
    for (Contacts contacts : contactSet) {
        System.out.println(contacts);
    }
}

查询一个联系人,获取该联系人的所有客户

@Test
@Transactional // 解决 no session 问题
public void findOneTest2(){
    Contacts contacts = contactsDao.findOne(1L);
    // 对象导航查询
    Customer customer = contacts.getCustomer();
    System.out.println(customer);
}

根据主表对象加载从表数据时,是延时加载(使用时发送sql查询)。根据从表对象加载主表数据时,是立即加载(一次查询出来)

对象导航查询的问题分析

问题1:我们查询客户时,要不要把联系人查询出来?

分析:如果我们不查的话,在用的时候还要自己写代码,调用方法去查询。如果我们查出来的,不使用时又会白白的浪费了服务器内存。

解决:采用延迟加载的思想。通过配置的方式来设定当我们在需要使用时,发起真正的查询。

配置方式:

/*    
 * fetch:
 *      FetchType.LAZY   延迟加载
 *      FetchType.EAGER  立即加载
 */
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Contacts> contactsSet = new HashSet<Contacts>();

问题2:我们查询联系人时,要不要把客户查询出来?

分析:例如:查询联系人详情时,肯定会看看该联系人的所属客户。如果我们不查的话,在用的时候还要自己写代码,调用方法去查询。如果我们查出来的话,一个对象不会消耗太多的内存。而且多数情况下我们都是要使用的。

解决: 采用立即加载的思想。通过配置的方式来设定,只要查询从表实体,就把主表实体对象同时查出来

配置方式:

@ManyToOne(targetEntity = Customer.class, fetch = FetchType.EAGER)
@JoinColumn(name = "cust_id", referencedColumnName = "custId")
private Customer customer;

5.2 使用Specification查询

/**
     * Specification的多表查询
     */
    @Test
    public void testFind(){
        Specification<Contacts> specification = new Specification<Contacts>() {
            @Override
            public Predicate toPredicate(Root<Contacts> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                // Join代表链接查询,通过root对象获取
                //创建的过程中,第一个参数为关联对象的属性名称,第二个参数为连接查询的方式(left,inner,right)
                //JoinType.LEFT : 左外连接,JoinType.INNER:内连接,JoinType.RIGHT:右外连接
                Join<Contacts, Customer> join = root.join("customer",JoinType.INNER);
                return cb.like(join.get("custName").as(String.class),"%云创%");
            }
        };
        List<Contacts> list = contactsDao.findAll(specification);
        for (Contacts contacts : list) {
            System.out.println(contacts);
        }
    }

标签

© 2021 成都云创动力科技有限公司 蜀ICP备20006351号-1