Spring Data JPA详细知识点,玩转JPA

Spring Data JPA

Spring Boot整合使用Spring Data JPA:

源代码仓库地址:

https://gitee.com/sc2glory/spring-data-jpa-knowledge.git

1. pom.xml配置:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.springdata</groupId>
    <artifactId>springdata</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springData</name>
    <description>Demo project for Spring Data</description>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    </properties>
    <!--boot依赖-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--将依赖项声明为可选-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!--spring data jpa 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--MySQL-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.23</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. YAML文件配置:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springdata
    username: root
    password: root
  # jpa数据库
  jpa:
    database: mysql
    # sql
    show-sql: true
    hibernate:
      ddl-auto: update

# tomcat
server:
  port: 80

# 日志
logging:
  level:
    com.springboot: debug
    org.springframework: info

3. 编写实体类:

package com.springdata.entity;

import javax.persistence.*;


/**
 * 1. 实体类和表的映射关系
 * @author GLoRy
 * @Entity,@Table 
 * 2. 类中属性和表中字段的映射关系
 * @Id @GeneratedValue @Column
 */
@Entity
@Table(name = "cst_customer")
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "custom_industry")
    private String customIndustry;
    @Column(name = "custom_address")
    private String customAddress;
    @Column(name = "custom_level")
    private String customLevel;
    @Column(name = "custom_name")
    private String customName;
    @Column(name = "custom_phone")
    private String customPhone;
    @Column(name = "custom_source")
    private String customSource;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getCustomIndustry() {
        return customIndustry;
    }

    public void setCustomIndustry(String customIndustry) {
        this.customIndustry = customIndustry;
    }

    public String getCustomAddress() {
        return customAddress;
    }

    public void setCustomAddress(String customAddress) {
        this.customAddress = customAddress;
    }

    public String getCustomLevel() {
        return customLevel;
    }

    public void setCustomLevel(String customLevel) {
        this.customLevel = customLevel;
    }

    public String getCustomName() {
        return customName;
    }

    public void setCustomName(String customName) {
        this.customName = customName;
    }

    public String getCustomPhone() {
        return customPhone;
    }

    public void setCustomPhone(String customPhone) {
        this.customPhone = customPhone;
    }

    public String getCustomSource() {
        return customSource;
    }

    public void setCustomSource(String customSource) {
        this.customSource = customSource;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "id=" + id +
                ", customIndustry='" + customIndustry + '\'' +
                ", customAddress='" + customAddress + '\'' +
                ", customLevel='" + customLevel + '\'' +
                ", customName='" + customName + '\'' +
                ", customPhone='" + customPhone + '\'' +
                ", customSource='" + customSource + '\'' +
                '}';
    }
}

4. 编写符合规范的DAO层接口:

package com.springdata.dao;

import com.springdata.entity.Customer;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation;

import java.util.List;

/**
 * 符合Spring Data Jpa 的 Dao层接口规范
 * JpaRepository<操作的实体类型,实体类中主键属性的类型> 封装了基本CRUD操作
 * JpaSpecificationExecutor<操作的实体类型> 封装了复杂查询(分页)
 * JpaRepositoryImplementation<操作的实体类型,实体类中主键属性的类型> 继承了上面两个接口
 *
 * @author GLoRy
 * @date 2021/5/11 22:43
 */
public interface CustomerRepository extends JpaRepositoryImplementation<Customer, Long> {

    /**
     * 根据客户名称查询客户
     * JPQL:from Customer where customName = ?
     * 注意:在注解中 ? 后面要加index,意思是代表方法中的第几个参数,否则报错,低版本spring可以不加
     *
     * @param customName 客户名称
     * @return 客户信息
     */
    @Query(value = "from Customer where customName = ?1")
    Customer findByName(String customName);

    /**
     * 根据客户id和名称查询客户信息
     * JPQL:from Customer where id = ?1 and customName = ?2
     *
     * @param id   客户id
     * @param name 姓名
     * @return 客户信息
     */
    @Query(value = "from Customer where id = ?1 and customName = ?2")
    Customer findByIdAndName(Long id, String name);

    /**
     * 根据id更新客户名称
     * SQL:update t_customer set custom_name = ? where id = ?;
     * JPQL:update Customer set customName = ?2 where id = ?1
     *
     * @param name 客户名称
     * @param id   客户id
     * @Query :用来进行查询操作,可以通过声明方法进行其他操作
     * @Modifying :声明当前的方法是更新操作
     */
    @Query(value = "update Customer set customName = ?2 where id = ?1")
    @Modifying
    void updateCustomerNameById(Long id, String name);

    /**
     * 使用SQL查询全部的客户信息
     * value:SQL语句
     * nativeQuery :true:使用本地查询SQL false:使用JPQL
     *
     * @return 全部客户信息
     */
    @Query(value = "select * from cst_customer", nativeQuery = true)
    List<Object[]> useSqlFindAll();

    /**
     * 使用SQL根据姓名模糊查询
     *
     * @param name 客户名称
     * @return 客户信息
     */
    @Query(value = "select * from cst_customer where custom_name like ?1", nativeQuery = true)
    List<Object[]> useSqlFindLikeName(String name);

    /**
     * 根据客户的姓名查询
     * findBy:查询
     * 查询的条件:对象中的属性名(首字母大写)
     * Spring Data JPA会解析:findBy(属性名称) from (实体类) where customName = ?
     * 默认方式:使用 "=" 的方式进行查询  findBy + 属性名称(根据属性名称完成匹配的查询)
     *
     * @param customName 客户名称
     * @return 客户信息
     */
    Customer findByCustomName(String customName);

    /**
     * 特殊的查询方式:findBy + 属性名称 + 查询方式(like | isnull 等等)
     * 模糊匹配查询
     *
     * @param customName 客户名称
     * @return 客户信息
     */
    List<Customer> findByCustomNameLike(String customName);

    /**
     * 多条件查询:findBy + 属性名称 + ”查询方式“ + 多条件的连接符(and | or)+ 属性名称 + ”查询方式“
     * 使用客户名称模糊匹配和来源精确匹配的查询
     *
     * @param customName   客户名称
     * @param customSource 客户来源
     * @return 客户
     */
    Customer findByCustomNameLikeAndCustomSource(String customName, String customSource);
}

5. 基础CRUD代码测试:

数据库表结构字段:

idcustom_industrycustom_levelcustom_addresscustom_namecustom_ponecustom_source
1IT行业good北京顾客00110001北京
2IT行业good南京顾客00210002南京
3IT行业good上海顾客00310003上海
4IT行业common成都顾客00410004成都
5IT行业good杭州顾客00510005杭州
6服务业good成都顾客006100006成都
8IT行业good深圳顾客00810008广州
package com.springdata;

import com.springdata.dao.CustomerRepository;
import com.springdata.entity.Customer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

/**
 * @author GLoRy
 * @date 2021/5/11 22:52
 */
@SpringBootTest
public class CustomerDaoTest {

    @Autowired
    private CustomerRepository customerRepository;

    /**
     * 保存或者更新
     * 根据是否存在主键id,如果没有id:保存
     * 如果存在id,根据id查询数据,更新数据
     */
    @Test
    void testSave() {
        Customer customer = new Customer();
        customer.setCustomIndustry("IT行业");
        customer.setCustomLevel("good");
        customer.setCustomAddress("北京");
        customer.setCustomName("顾客001");
        customer.setCustomPhone("10001");
        customer.setCustomSource("北京");
        customerRepository.save(customer);
    }

    /**
     * 更新
     */
    @Test
    void testUpdate() {
        Customer customer = new Customer();
        customer.setId(8L);
        customer.setCustomIndustry("IT行业");
        customer.setCustomLevel("good");
        customer.setCustomAddress("深圳");
        customer.setCustomName("顾客008");
        customer.setCustomPhone("10008");
        customer.setCustomSource("广州");
        customerRepository.save(customer);
    }

    /**
     * 删除
     */
    @Test
    void deleteTest() {
        customerRepository.deleteById(7L);
    }

    /**
     * 根据id查询
     */
    @Test
    void testFindById() {
        Optional<Customer> byId = customerRepository.findById(2L);
        System.out.println(byId);
    }

    /**
     * 查询所有
     */
    @Test
    void findAllTest() {
        List<Customer> customers = customerRepository.findAll();
        for (Customer customer : customers) {
            System.out.println(customer);
        }
    }

    /**
     * 统计查询总数量
     */
    @Test
    void testCount() {
        long count = customerRepository.count();
        System.out.println(count);
    }

    /**
     * 判断id为2的客户是否存在
     * 1. 查询用户id为2是否存在,如果为null就不存在,否则存在
     * 2. 判断数据库里id为2的客户数量。数量为0不存在,大于0存在
     * jpa使用的第二种方式
     */
    @Test
    void testExistById() {
        boolean exist = customerRepository.existsById(2L);
        System.out.println(exist);
    }

    /**
     * 根据id从数据库查询
     *
     * @Transactional 保证getOne()正常运行
     * findOne():调用的是em.find() 立即加载
     * getOne():调用的是em.getReference() 延迟加载 返回的是一个动态代理对象,什么时候就用什么时候查询
     */
    @Test
    @Transactional
    void testGetOne() {
        Customer customer = customerRepository.getOne(2L);
        System.out.println(customer);
    }
}

6. 基本CRUD的执行过程原理:

UML图展示:

源码截图:

以findById()方法为例,执行过程如下:

  1. 在findById方法上打断点

  1. 进入JdkDynamicAopProxy方法,可以看到动态代理相关内容

  1. 通过JdkDynamicAopProxy创建动态代理对象

代理对象:target

  1. 进入到实现类方法SimpleJpaRepository,其中封装了JPA的操作,图中进入到findById()方法

  1. 执行EntityManager下的em方法完成方法功能

7. 复杂查询:

  • 借助接口中定义好的方法进行查询
  • JPQL的方式完成查询
    • JPQL:JPA Query Language(JPA查询语言)
    • 特点:语法或者关键字和SQL语句类似
      • 查询的是类和类中的属性
    • 需要将JPQL语句配置到接口方法上
      • 特有的查询:需要在DAO层接口上配置查询方式
      • 在新添加的方法上,使用注解的形式配置JPQL查询语句
      • 注解:@Query
  • SQL语句的查询
    • 需要将SQL语句配置到接口方法上
      • 特有的查询:需要在DAO层接口上配置查询方式
      • 在新添加的方法上,使用注解的形式配置SQL查询语句
      • 注解:@Query
        • value:JPQL | SQL
        • nativeQuery :false | true 代表是否是使用本地查询
          • true:使用本地查询SQL
          • false:使用JPQL
  • 方法名称规则查询
    • 这种方法是对JPQL查询更深入的一层封装
    • 只需按照Spring Data JPA提供的方法名称规则定义方法,不需要再去配置JPQL语句,就可以完成查询操作
    • 命名约定:
      • findBy开头:代表查询
        • 查询的条件:对象中的属性名(首字母大写)
        • 根据属性名称进行查询
          • 例如findByCustomName – 根据客户的姓名查询
        • 在Spring Data JPA的运行阶段,会根据方法名进行解析
          • findBy(属性名称) from (实体类) where customName = ?
        • 默认方式:使用 “=” 的方式进行查询 findBy + 属性名称(根据属性名称完成匹配的查询)
        • 特殊的查询方式:findBy + 属性名称 + 查询方式(like | isnull 等等)
        • 多条件查询:findBy + 属性名称 + ”查询方式“ + 多条件的连接符(and | or)+ 属性名称 + ”查询方式“
JPQL,SQL和方法名称规则代码测试:

JPQL和SQL查询代码:

package com.springdata;

import com.springdata.dao.CustomerRepository;
import com.springdata.entity.Customer;
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 org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.List;

/**
 * @author GLoRy
 * @date 2021/5/12 14:35
 */
@SpringBootTest
public class JpqlTest {

    @Autowired
    private CustomerRepository customerRepository;

    /**
     * JPQL 通过名字查询
     */
    @Test
    void testFindByName() {
        Customer customer = customerRepository.findByName("顾客002");
        System.out.println(customer);
    }

    /**
     * JPQL 通过id和名字查询
     */
    @Test
    void testFindByIdAndName() {
        Customer customer = customerRepository.findByIdAndName(2L, "顾客002");
        System.out.println(customer);
    }

    /**
     * JPQl 通过id更新名称
     *
     * @Transactional 进行更新或者删除操作需要加上事务注解,默认执行之后回滚事务
     * @Rollback :设置是否自动回滚
     */
    @Test
    @Transactional
    @Rollback(value = false)
    void updateCustomerNameById() {
        customerRepository.updateCustomerNameById(6L, "顾客0006");
    }

    /**
     * 使用SQL查询全部
     */
    @Test
    void useSqlFindAll() {
        List<Object[]> lists = customerRepository.useSqlFindAll();
        for (Object[] list : lists) {
            System.out.println(Arrays.toString(list));
        }
    }

    /**
     * 使用SQL根据姓名模糊查询
     */
    @Test
    void useSqlFindLikeName() {
        List<Object[]> lists = customerRepository.useSqlFindLikeName("%000%");
        for (Object[] list : lists) {
            System.out.println(Arrays.toString(list));
        }
    }
}

方法名称规则查询代码:

package com.springdata;

import com.springdata.dao.CustomerRepository;
import com.springdata.entity.Customer;
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
class NamingRulesTest {

    @Autowired
    private CustomerRepository customerRepository;

    /**
     * 通过姓名查询
     */
    @Test
    void findByCustomName() {
        Customer customer = customerRepository.findByCustomName("顾客002");
        System.out.println(customer);
    }

    /**
     * 模糊匹配查询
     */
    @Test
    void testFindByCustomNameLike() {
        List<Customer> customers = customerRepository.findByCustomNameLike("顾客%");
        for (Customer customer : customers) {
            System.out.println(customer);
        }
    }

    /**
     * 使用客户名称模糊匹配和来源精确匹配的查询
     */
    @Test
    void testFindByCustomNameLikeAndCustomSource() {
        Customer customer = customerRepository.findByCustomNameLikeAndCustomSource("顾客%", "南京");
        System.out.println(customer);
    }
}
Specifications动态查询
方法:
//查询单个对象
Optional<T> findOne(@Nullable Specification<T> var1);

//查询全部
List<T> findAll(@Nullable Specification<T> var1);

/*
 * 查询全部并分页
 * Pageable:分页参数
 * 返回值:分页PageBean(spring data jpa提供)
 */
Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);

/*
 * 查询全部并排序
 * Sort:分页参数
 */
List<T> findAll(@Nullable Specification<T> var1, Sort var2);

//统计查询
long count(@Nullable Specification<T> var1);
详细代码测试以及注释:
package com.springdata;

import com.springdata.dao.CustomerRepository;
import com.springdata.entity.Customer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.*;
import java.util.List;
import java.util.Optional;

/**
 * Specification测试
 *
 * @author GLoRy
 * @date 2021/5/13 13:48
 */
@SpringBootTest
public class SpecificationTest {

    @Autowired
    private CustomerRepository customerRepository;

    /**
     * 根据条件,查询单个对象
     */
    @Test
    void testSpecification1() {
        /*
         * 匿名内部类
         * 自定义查询条件
         * 1. 实现Specification<T>接口
         * 2. 实现toPredicate()方法(构造查询条件)
         * 3. 需要借助方法中的两个参数(
         *        Root:获取需要查询的属性
         *        CriteriaBuilder:构造查询条件,内部封装了如(模糊匹配,精准匹配等)
         *    )
         * 例如:根据客户名称查询
         *     查询条件
         *         1. 查询方式:
         *            criteriaBuilder对象
         *         2. 比较的属性名称
         *            root对象中
         */
        Specification<Customer> specification = (root, criteriaQuery, criteriaBuilder) -> {
            //1.获取比较的属性
            Path<Object> customName = root.get("customName");
            /*
             *   2.构造查询条件
             *   第一个参数:需要比较的属性(path对象)
             *   第二个参数:当前需要比较的取值
             */
            return criteriaBuilder.equal(customName, "顾客002");
        };
        Optional<Customer> customer = customerRepository.findOne(specification);
        System.out.println(customer);
    }

    /**
     * 多条件查询
     * 根据客户名和客户所属行业查询
     */
    @Test
    void testSpecification2() {
        /*
         * root:获取属性(客户名称,客户行业)
         * criteriaBuilder:构造查询
         *   1.构造客户名的精准匹配查询
         *   2.构造所属行业的精准匹配查询
         *   3.将上面两个查询联系起来
         * */
        Specification<Customer> specification = (root, criteriaQuery, criteriaBuilder) -> {
            Path<Object> customName = root.get("customName");//客户名称
            Path<Object> customIndustry = root.get("customIndustry");//所属行业
            Predicate predicate1 = criteriaBuilder.equal(customName, "顾客003");
            Predicate predicate2 = criteriaBuilder.equal(customIndustry, "IT行业");
            /*
             * 将多个查询条件组合到一起
             * 满足条件一且满足条件二 “与”关系 and
             * 满足条件一或满足条件二 “或”关系 or
             */
            return criteriaBuilder.and(predicate1, predicate2);
        };
        Optional<Customer> customer = customerRepository.findOne(specification);
        System.out.println(customer);
    }

    /**
     * 根据客户名称模糊查询,返回客户集合
     * 查询属性:客户名称
     * 查询方式:模糊查询
     * equal:直接得到path对象,然后进行比较即可
     * like,gt,ge,le,lt等等:得到path对象,根据path指定比较的参数类型,再进行比较 path.as(类型的字节码对象)
     */
    @Test
    void testSpecification3() {
        Specification<Customer> specification = (root, criteriaQuery, criteriaBuilder) -> {
            Path<Object> customName = root.get("customName");
            return criteriaBuilder.like(customName.as(String.class), "顾客%");
        };
        List<Customer> customerList = customerRepository.findAll(specification);
        for (Customer customer : customerList) {
            System.out.println(customer);
        }
    }

    /**
     * 查询并排序,需要调用构造方法实例化对象
     * (ASC升序,DESC倒序)
     */
    @Test
    void testSpecificationSort() {
        Specification<Customer> specification = (root, criteriaQuery, criteriaBuilder) -> {
            Path<Object> customName = root.get("customName");
            Predicate predicate = criteriaBuilder.like(customName.as(String.class), "顾客%");
            return criteriaQuery.where(predicate).orderBy(criteriaBuilder.desc
            	(root.get("id"))).getRestriction();
        };
        List<Customer> customerList = customerRepository.findAll(specification);
        for (Customer customer : customerList) {
            System.out.println(customer);
        }
    }

    /**
     * 分页查询
     * Specification;查询条件
     * Pageable:分页参数(当前页码,每页查询条数)
     * findAll(Specification,Pageable) 条件分页
     * findAll(Specification) 无条件分页
     * 返回:Page对象(spring data jpa封装好的PageBean,可以获取数据列表,总条数等等)
     */
    @Test
    void testPageSpecification() {
        /*
         * PageRequest对象是Pageable的实现类
         * 创建PageRequest,需要调用它的构造方法传入参数
         * 第一个参数:当前查询的页数(从0开始)
         * 第二个参数:每页查询的数量
         */
        //两种方式构建排序对象
        Sort sort = Sort.by(Sort.Direction.DESC, "id");
        Sort.Order order = new Sort.Order(Sort.Direction.DESC, "id");
        //PageRequest传入参数,不带排序
        PageRequest pageRequest = PageRequest.of(0, 2);
        //PageRequest传入参数,排序
        PageRequest pageRequest1 = PageRequest.of(0, 2, sort);
        PageRequest pageRequest2 = PageRequest.of(0, 2, Sort.by(order));
        //创建page对象
        Page<Customer> page = customerRepository.findAll((Specification<Customer>) null, pageRequest);
        Page<Customer> page1 = customerRepository.findAll((Specification<Customer>) null, pageRequest1);
        Page<Customer> page2 = customerRepository.findAll((Specification<Customer>) null, pageRequest2);
        System.out.println("不排序的总页数" + page.getTotalPages());
        System.out.println("不排序的总条数" + page.getTotalElements());
        System.out.println("第一种排序方式的总页数" + page1.getTotalPages());
        System.out.println("第一种排序方式的总条数" + page1.getTotalElements());
        System.out.println("第二种排序方式的总页数" + page2.getTotalPages());
        System.out.println("第二种排序方式的总条数" + page2.getTotalElements());
        //打印没有排序的客户信息列表
        List<Customer> pageContent = page.getContent();
        for (Customer customer : pageContent) {
            System.out.println("非排序的客户信息:" + customer);
        }
        //打印降序排序的客户信息列表
        List<Customer> pageContent1 = page1.getContent();
        for (Customer customer1 : pageContent1) {
            System.out.println("降序的客户信息:" + customer1);
        }
    }

    /**
     * 使用criteriaQuery方法
     * 模糊查询,分页,排序
     */
    @Test
    void testPageOrder() {0
        Page<Customer> page = customerRepository.findAll((Specification<Customer>) (root, criteriaQuery, 				criteriaBuilder) -> {
            Path<Object> customIndustry = root.get("customIndustry");
            //模糊匹配条件
            Predicate predicate1 = criteriaBuilder.like(customIndustry.as(String.class), "IT%");
            return criteriaQuery.where(predicate1).orderBy(criteriaBuilder.desc(root.get("id")))
                .getRestriction();
        }, PageRequest.of(0, 2));
        System.out.println("总条数" + page.getTotalElements());
        System.out.println("总页数" + page.getTotalPages());
        //获取查询内容
        List<Customer> pageContent = page.getContent();
        for (Customer customer : pageContent) {
            System.out.println(customer);
        }
    }
}

8. 多表之间的关系和操作步骤

多表之间的关系
  • 一对一:在实际的开发中应用不多,因为一对一可以创建成一张表。
  • 一对多:
    • “一”的一方:主表
    • “多”的一方:从表
    • 外键:需要从“多”的一方建立外键,取值于主表的主键
    • 例如:老师和学生
  • 多对多:
    • 中间表:最少由两个字段组成,两个字段作为外键指向两张表的主键,又组成了联合主键
    • 例如:学生和课程
实体类中的关系
  • 包含关系:可以通过实体类中的包含关系描述表关系
  • 继承关系
分析步骤
  1. 明确了解表关系
  2. 确定表关系(描述 外键|中间表)
  3. 编写实体类,在实体类中描述表关系(包含关系)
  4. 配置映射关系
一对多
  • 案例:客户和联系人
    • 客户:代表一家公司 一个客户可以有多个联系人
    • 联系人:公司的员工 一个联系人从属于一家公司
  • 分析:
    • 表关系:一对多
      2. 表关系:
      • 主表:客户表
      • 从表:联系人表,在从表添加外键
      1. 编写实体类,描述包含关系
        • 客户:在客户的实体类中包含一个联系人的集合
        • 联系人:在联系人的实体类中包含一个客户的对象
      2. 配置映射关系
        • 使用JPA注解配置一对多映射关系
  • 级联:
    • 级联操作
      • 需要区分操作主体
      • 需要在操作主体的实体类上添加级联属性(需要添加到多表映射关系的注解上)
      • cascade(配置级联)
    • 操作一个对象的同时操作它的关联对象,如级联添加,删除,更新等
      • 级联添加:当保存一个主表数据的同时保存从表数据
      • 级联删除:当删除一个主表数据的同时删除从表的所有关联数据
详细代码以及注释:
实体类代码:
package com.springdata.multitablejpa.entity;

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

import static sun.management.snmp.jvminstr.JvmThreadInstanceEntryImpl.ThreadStateMap.Byte1.other;

/**
 * 客户(主表)
 *
 * @author GLoRy
 * @date 2021/5/13 21:39
 */
@Entity
@Table(name = "t_customer")
public class MultiCustomer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "customer_id")
    private Long customerId;

    @Column(name = "customer_name")
    private String customerName;

    @Column(name = "customer_source")
    private String customerSource;

    @Column(name = "customer_industry")
    private String customerIndustry;

    @Column(name = "customer_level")
    private String customerLevel;

    @Column(name = "customer_address")
    private String customerAddress;

    @Column(name = "customer_phone")
    private String customerPhone;

    /**
     * 配置客户和联系人之间的关系(一对多)
     * 在客户实体类上(”主表“)添加外键配置,所以具备了维护外键的作用
     * 使用注解形式配置
     * 1.声明关系
     * 2.配置(外键|中间表)
     *
     * @OneToMany(targetEntity = MultiContact.class)
     * @JoinColumn(name = "contact_customer_id", referencedColumnName = "customer_id")
     * @OneToMany :配置一对多关系
     * targetEntity :对方对象的字节码对象
     * @JoinColumn :配置外键
     * name:外键字段名称
     * referencedColumnName:参照的主表主键字段名称
     * ---------------------------------------------------------
     * @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
     * 放弃外键维护权,mapperBy的取值为对方配置关系的属性名称
     * cascade = CascadeType.ALL 设置级联类型,例如All(所有),MERGE(更新),PERSIST(保存),REMOVE(删除)
     * fetch:配置关联对象的加载方式,EAGER(立即加载),LAZY(延迟加载)
     */
    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
    private Set<MultiContact> multiContacts = new HashSet<>();

    public Set<MultiContact> getMultiContacts() {
        return multiContacts;
    }

    public void setMultiContacts(Set<MultiContact> multiContacts) {
        this.multiContacts = multiContacts;
    }

    public Long getCustomerId() {
        return customerId;
    }

    public void setCustomerId(Long customerId) {
        this.customerId = customerId;
    }

    public String getCustomerName() {
        return customerName;
    }

    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }

    public String getCustomerSource() {
        return customerSource;
    }

    public void setCustomerSource(String customerSource) {
        this.customerSource = customerSource;
    }

    public String getCustomerIndustry() {
        return customerIndustry;
    }

    public void setCustomerIndustry(String customerIndustry) {
        this.customerIndustry = customerIndustry;
    }

    public String getCustomerLevel() {
        return customerLevel;
    }

    public void setCustomerLevel(String customerLevel) {
        this.customerLevel = customerLevel;
    }

    public String getCustomerAddress() {
        return customerAddress;
    }

    public void setCustomerAddress(String customerAddress) {
        this.customerAddress = customerAddress;
    }

    public String getCustomerPhone() {
        return customerPhone;
    }

    public void setCustomerPhone(String customerPhone) {
        this.customerPhone = customerPhone;
    }

    @Override
    public String toString() {
        return "MultiCustomer{" +
                "customerId=" + customerId +
                ", customerName='" + customerName + '\'' +
                ", customerSource='" + customerSource + '\'' +
                ", customerIndustry='" + customerIndustry + '\'' +
                ", customerLevel='" + customerLevel + '\'' +
                ", customerAddress='" + customerAddress + '\'' +
                ", customerPhone='" + customerPhone + '\'' +
                '}';
    }
}
package com.springdata.multitablejpa.entity;

import javax.persistence.*;
import java.util.Objects;

/**
 * 联系人(从表)
 *
 * @author GLoRy
 * @date 2021/5/13 21:43
 */
@Entity
@Table(name = "t_contact")
public class MultiContact {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "contact_id")
    private Long contactId;

    @Column(name = "contact_name")
    private String contactName;

    @Column(name = "contact_gender")
    private String contactGender;

    @Column(name = "contact_phone")
    private String contactPhone;

    @Column(name = "contact_mobile")
    private String contactMobile;

    @Column(name = "contact_email")
    private String contactEmail;

    @Column(name = "contact_position")
    private String contactPosition;

    @Column(name = "contact_remark")
    private String contactRemark;

    /**
     * 配置联系人到客户的多对一关系
     * 配置外键的过程,配置到了多的一方,就会在多的一方维护外键
     * 注解形式配置多对一
     * 1.配置表关系
     * 2.配置"外键|中间表"
     *
     * @ManyToOne :配置多对一关系
     * targetEntity : 配置对方对象的实体类字节码
     */
    @ManyToOne(targetEntity = MultiCustomer.class,fetch = FetchType.LAZY)
    @JoinColumn(name = "contact_customer_id", referencedColumnName = "customer_id")
    private MultiCustomer customer;

    public MultiCustomer getCustomer() {
        return customer;
    }

    public void setCustomer(MultiCustomer customer) {
        this.customer = customer;
    }

    public Long getContactId() {
        return contactId;
    }

    public void setContactId(Long contactId) {
        this.contactId = contactId;
    }

    public String getContactName() {
        return contactName;
    }

    public void setContactName(String contactName) {
        this.contactName = contactName;
    }

    public String getContactGender() {
        return contactGender;
    }

    public void setContactGender(String contactGender) {
        this.contactGender = contactGender;
    }

    public String getContactPhone() {
        return contactPhone;
    }

    public void setContactPhone(String contactPhone) {
        this.contactPhone = contactPhone;
    }

    public String getContactMobile() {
        return contactMobile;
    }

    public void setContactMobile(String contactMobile) {
        this.contactMobile = contactMobile;
    }

    public String getContactEmail() {
        return contactEmail;
    }

    public void setContactEmail(String contactEmail) {
        this.contactEmail = contactEmail;
    }

    public String getContactPosition() {
        return contactPosition;
    }

    public void setContactPosition(String contactPosition) {
        this.contactPosition = contactPosition;
    }

    public String getContactRemark() {
        return contactRemark;
    }

    public void setContactRemark(String contactRemark) {
        this.contactRemark = contactRemark;
    }

    @Override
    public String toString() {
        return "MultiContact{" +
                "contactId=" + contactId +
                ", contactName='" + contactName + '\'' +
                ", contactGender='" + contactGender + '\'' +
                ", contactPhone='" + contactPhone + '\'' +
                ", contactMobile='" + contactMobile + '\'' +
                ", contactEmail='" + contactEmail + '\'' +
                ", contactPosition='" + contactPosition + '\'' +
                ", contactRemark='" + contactRemark + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        MultiContact that = (MultiContact) o;
        return customer.equals(that.customer);
    }

    @Override
    public int hashCode() {
        return Objects.hash(customer);
    }
}
DAO接口代码:
package com.springdata.multitablejpa.dao;

import com.springdata.multitablejpa.entity.MultiCustomer;
import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation;

/**
 * @author GLoRy
 * @date 2021/5/13 21:59
 */
public interface MultiCustomerRepository extends JpaRepositoryImplementation<MultiCustomer, Long> {
}
package com.springdata.multitablejpa.dao;

import com.springdata.multitablejpa.entity.MultiContact;
import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation;

/**
 * @author GLoRy
 * @date 2021/5/13 21:59
 */
public interface MultiContactRepository extends JpaRepositoryImplementation<MultiContact, Long> {
}
测试代码:
package com.springdata.multitablejpa.onetomany;

import com.springdata.multitablejpa.dao.MultiContactRepository;
import com.springdata.multitablejpa.dao.MultiCustomerRepository;
import com.springdata.multitablejpa.entity.MultiContact;
import com.springdata.multitablejpa.entity.MultiCustomer;
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 org.springframework.transaction.annotation.Transactional;

/**
 * 一对多测试
 *
 * @author GLoRy
 * @date 2021/5/14 9:48
 */
@SpringBootTest
public class OneToManyTest {

    @Autowired
    private MultiCustomerRepository multiCustomerRepository;

    @Autowired
    private MultiContactRepository multiContactRepository;

    /**
     * 保存一个客户和一个联系人
     */
    @Test
    @Transactional
    @Rollback(value = false)
    void testSave1() {
        MultiCustomer multiCustomer = new MultiCustomer();
        multiCustomer.setCustomerName("公司1");

        MultiContact multiContact = new MultiContact();
        multiContact.setContactName("张三");

        /*
         * 配置客户到联系人的关系(一对多)
         * 客户:执行了两条insert语句和一条更新语句(更新外键)
         * 由于配置了对象的主从关系,主表对象可以对外键进行维护
         */
        multiCustomer.getMultiContacts().add(multiContact);

        multiCustomerRepository.save(multiCustomer);
        multiContactRepository.save(multiContact);
    }

    /**
     * 保存一个客户和一个联系人
     */
    @Test
    @Transactional
    @Rollback(value = false)
    void testSave2() {
        MultiCustomer multiCustomer = new MultiCustomer();
        multiCustomer.setCustomerName("公司1");

        MultiContact multiContact = new MultiContact();
        multiContact.setContactName("张三");

        /*
         * 配置联客人到客户的关系(多对一)
         * 只执行了两条insert语句
         * 由于配置了联系人到客户的映射关系,所以可以直接维护外键
         */
        multiContact.setCustomer(multiCustomer);

        multiCustomerRepository.save(multiCustomer);
        multiContactRepository.save(multiContact);
    }

    /**
     * 保存一个客户和一个联系人
     */
    @Test
    @Transactional
    @Rollback(value = false)
    void testSave3() {
        MultiCustomer multiCustomer = new MultiCustomer();
        multiCustomer.setCustomerName("公司1");

        MultiContact multiContact = new MultiContact();
        multiContact.setContactName("张三");

        /*
         * 两种关系都配置
         * 由于配置了多对一的关联关系,当保存的时候就已经对外键赋值
         * 由于配置了一对多的关联关系,会发送update语句
         * 会有一条多余的update语句,因为“一”的一方可以维护外键
         *     解决方案:只需在“一”的一方放弃维护权即可
         */
        multiContact.setCustomer(multiCustomer);
        multiCustomer.getMultiContacts().add(multiContact);

        multiCustomerRepository.save(multiCustomer);
        multiContactRepository.save(multiContact);
    }

    /**
     * 级联添加
     * 需要在操作主体的实体类上,配置cascade属性
     */
    @Test
    @Transactional
    @Rollback(value = false)
    void testCascadeAdd() {
        MultiCustomer multiCustomer = new MultiCustomer();
        multiCustomer.setCustomerName("公司2");

        MultiContact multiContact = new MultiContact();
        multiContact.setContactName("李四");

        multiContact.setCustomer(multiCustomer);
        multiCustomer.getMultiContacts().add(multiContact);

        multiCustomerRepository.save(multiCustomer);
    }

    /**
     * 级联删除
     * 案例:删除id为1客户的同时删除所有对应联系人
     */
    @Test
    @Transactional
    @Rollback(value = false)
    void testCascadeRemove() {
        //1. 查询客户
        MultiCustomer customer = multiCustomerRepository.getOne(1L);
        //2. 删除客户
        multiCustomerRepository.delete(customer);
    }
}
多对多
  • 用户和角色(多对多)

  • 分析:

    1. 表关系:多对多

    2. 确定表关系:中间表

    3. 编写实体类:

      • 用户:包含角色的集合
      • 角色:包含用户的集合
    4. 配置映射关系:

      • 使用JPA注解配置多对多映射关系
详细代码以及注释:
实体类代码:
package com.springdata.multitablejpa.entity;

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

/**
 * 多对多 角色实体
 *
 * @author GLoRy
 * @date 2021/5/14 13:03
 */
@Entity
@Table(name = "t_role")
public class RoleMany {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "role_id")
    private Long roleId;

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

    /**
     * 配置角色到用户的多对多关系
     * 被动的一方,比如角色放弃维护权
     *
     * @ManyToMany(targetEntity = UserMany.class)
     * @JoinTable(name = "t_user_role",
     * joinColumns = {@JoinColumn(name = "t_role_id", referencedColumnName = "role_id")},
     * inverseJoinColumns = {@JoinColumn(name = "t_user_id", referencedColumnName = "user_id")}
     * )
     */
    @ManyToMany(mappedBy = "roles")
    private Set<UserMany> users = new HashSet<>();

    public Set<UserMany> getUsers() {
        return users;
    }

    public void setUsers(Set<UserMany> users) {
        this.users = users;
    }

    public Long getRoleId() {
        return roleId;
    }

    public void setRoleId(Long roleId) {
        this.roleId = roleId;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        RoleMany roleMany = (RoleMany) o;
        return users.equals(roleMany.users);
    }

    @Override
    public int hashCode() {
        return Objects.hash(users);
    }
}
package com.springdata.multitablejpa.entity;

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

/**
 * 多对多 用户实体
 *
 * @author GLoRy
 * @date 2021/5/14 13:02
 */
@Entity
@Table(name = "t_user")
public class UserMany {
    @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;

    /**
     * 配置用户到角色的多对多关系
     * 1. 声明表关系的配置
     * 2. 配置中间表(包含2个外键)
     *
     * @JoinTable name:中间表的名称
     * joinColumns;当前对象在中间表中的外键
     * inverseJoinColumns;对方对象在中间表的外键
     */
    @ManyToMany(targetEntity = RoleMany.class, cascade = CascadeType.ALL)
    @JoinTable(name = "t_user_role",
            joinColumns = {@JoinColumn(name = "t_user_id", referencedColumnName = "user_id")},
            inverseJoinColumns = {@JoinColumn(name = "t_role_id", referencedColumnName = "role_id")}
    )
    private Set<RoleMany> roles = new HashSet<>();

    public Set<RoleMany> getRoles() {
        return roles;
    }

    public void setRoles(Set<RoleMany> roles) {
        this.roles = roles;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        UserMany userMany = (UserMany) o;
        return roles.equals(userMany.roles);
    }

    @Override
    public int hashCode() {
        return Objects.hash(roles);
    }
}
DAO接口代码:
package com.springdata.multitablejpa.dao;

import com.springdata.multitablejpa.entity.UserMany;
import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation;

/**
 * 用户数据库接口
 *
 * @author GLoRy
 * @date 2021/5/14 13:09
 */
public interface UserRepository extends JpaRepositoryImplementation<UserMany, Long> {
}
package com.springdata.multitablejpa.dao;

import com.springdata.multitablejpa.entity.RoleMany;
import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation;

/**
 * 角色数据库接口
 *
 * @author GLoRy
 * @date 2021/5/14 13:09
 */
public interface RoleRepository extends JpaRepositoryImplementation<RoleMany, Long> {
}
测试代码:
package com.springdata.multitablejpa.manytomany;

import com.springdata.multitablejpa.dao.RoleRepository;
import com.springdata.multitablejpa.dao.UserRepository;
import com.springdata.multitablejpa.entity.RoleMany;
import com.springdata.multitablejpa.entity.UserMany;
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 org.springframework.transaction.annotation.Transactional;

/**
 * 多对多测试
 */
@SpringBootTest
class ManyToManyTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    /**
     * 保存一个用户和角色
     * 多对多放弃维护权:被动的一放弃
     */
    @Test
    @Transactional
    @Rollback(value = false)
    void testSave() {
        UserMany user = new UserMany();
        user.setUserName("张三");
        user.setAge(18);

        RoleMany role = new RoleMany();
        role.setRoleName("Java程序员");

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

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

    /**
     * 测试级联添加
     * 测试:保存一个用户的同时保存用户的关联角色
     */
    @Test
    @Transactional
    @Rollback(value = false)
    void testCascadeSave() {
        UserMany user = new UserMany();
        user.setUserName("张三");
        user.setAge(18);

        RoleMany role = new RoleMany();
        role.setRoleName("爪哇程序员");

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

        userRepository.save(user);
    }

    /**
     * 测试级联删除
     * 测试:删除id为1的用户的同时删除用户的关联角色
     */
    @Test
    @Transactional
    @Rollback(value = false)
    void testCascadeDelete() {
        //查询id为1的用户
        UserMany user = userRepository.getOne(1L);
        //删除用户
        userRepository.delete(user);
    }
}
多表查询
  • 对象导航查询
    • 查询一个对象的同时,通过此对象查询它的关联对象
    • 案例:一对多形式,比较有代表性(使用上面一对多的例子,客户和联系人)
      • “一”方查询“多”方:默认使用延迟加载
      • “多”方查询“一”方:默认使用立即加载
测试代码及注释示例:
package com.springdata.multitablejpa.query;

import com.springdata.multitablejpa.dao.MultiContactRepository;
import com.springdata.multitablejpa.dao.MultiCustomerRepository;
import com.springdata.multitablejpa.entity.MultiContact;
import com.springdata.multitablejpa.entity.MultiCustomer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.Set;

/**
 * 对象导航查询测试
 * 查询一个对象的同时,通过此对象查询它的关联对象
 *
 * @author GLoRy
 * @date 2021/5/14 14:04
 */
@SpringBootTest
public class ObjectQueryTest {

    /*
     * 对象导航查询:
     *    一对多默认使用的延迟加载形式查询,调用get方法不会立即加载,而是在使用关联对象的时候才会查询
     *    多对一默认使用的立即加载形式查询
     * 解决方案:
     *    修改配置,将延迟加载改为(立即|延迟)加载
     *    fetch 需要配置到多表映射关系的注解上
     */

    @Autowired
    private MultiCustomerRepository multiCustomerRepository;

    @Autowired
    private MultiContactRepository multiContactRepository;

    /**
     * 查询id为1的客户,并导航查询所有的联系人
     *
     * @Transactional :解决no session的问题
     */
    @Test
    @Transactional
    void testQuery1() {
        //查询id为1的客户
        MultiCustomer customer = multiCustomerRepository.getOne(1L);
        //对象导航查询
        Set<MultiContact> contacts = customer.getMultiContacts();
        for (MultiContact contact : contacts) {
            System.out.println(contact);
        }
    }

    /**
     * 从联系人导航查询所属客户
     */
    @Test
    @Transactional
    void testQuery2() {
        MultiContact contact = multiContactRepository.getOne(2L);
        //对象导航查询
        MultiCustomer customer = contact.getCustomer();
        System.out.println(customer);
    }
}
  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值