SpringData

本文详细介绍了SpringData JPA,包括SpringData的背景、JPA回顾、SpringData JPA的基础知识和实战。通过实例演示了JPA的增删改查操作,探讨了SpringData JPA的运行原理,详细阐述了多表操作,如一对一、一对多和多对多关系的配置与实现。适合希望深入理解SpringData JPA的开发者阅读。
摘要由CSDN通过智能技术生成

1.SpringData概述

1.1.持久层开发的问题

        随着互联网技术的发展,现在的企业开发中用到的用于数据存储的产品,不再仅仅是关系型数据库,而是要根据场景需要选择不同的存储技术,比如用于缓存热点数据的redis,用于存储文档数据的mongodb,用于支持强大搜索功能的elasticsearch等等。

        在Java中,对于上面所说的产品都提供了优秀的访问技术。比如针对关系型数据库mybatis、jpa等技术,针对于redis的jedis技术等等..... 这些技术虽然可以很好的针对各个存储产品进行访问操作,但同时也带来了新的问题,那就是不同的持久层技术的API是不一样的。

        这样一来,开发人员就必须同时掌握多种数据访问技术,这无疑增加了开发成本。那么我们会想,有没有这样一种技术,它可以使用一套API支持各个不同的存储的访问呢?就在这样的需求下,SpringData产生了。

1.2.SpringData简介

1.2.1.什么是SpringData

        SpringData是一个用来简化dao层开发的框架。它在保证了各个底层存储特性的同时,提供了一套统一的数据访问API。它可以很好的支持常用的关系型数据库和非关系型数据库。

        使用SpringData作为dao层开发技术,将大大简化代码量,而且其API比各个技术的原生API更加简单易用。

1.2.2.SpringData的主要模块

SpringData支持的持久层技术非常多,我们只介绍几个常见的:

  • Spring Data common           SpringData的核心模块,定义了SpringData的核心功能
  • Spring Data JDBC                对JDBC的Spring Data存储库支持
  • Spring Data JPA                    对JPA的Spring Data存储库支持
  • Spring Data MongoDB          对MongoDB的基于Spring对象文档的存储库支持
  • Spring Data Redis                封装Jedis技术,对redis实现访问操作
  • Spring Data Elasticsearch    对Elasticsearch实现访问操作

2.JPA回顾

2.1.JPA基础

JPA 的全称是 Java Persistence API,即 Java 持久化 API,是 SUN 公司推出的一套基于 ORM 的规范,注意不是 ORM 框架——因为 JPA 并未提供 ORM 实现,它只是提供了一些编程的 API 接口。

2.2.JPA实战

2.2.1.目标

本章节我们是实现的功能是搭建Jpa环境,并实现一条数据的增删改查。

目录解构:

2.2.2.准备数据库环境

--准备数据库,创建一张文章表备用
CREATE TABLE `article` (
`aid` int(11) NOT NULL auto_increment COMMENT '主键',
`author` varchar(255) default NULL COMMENT '作者',
`createTime` datetime default NULL COMMENT '创建时间',
`title` varchar(255) default NULL COMMENT '标题',
PRIMARY KEY (`aid`)
);

2.2.3.创建java工程,导入坐标

<dependencies>
        <!--Jpa的支撑框架hibernate-->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-c3p0</artifactId>
            <version>5.6.15.Final</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <!-- 日志 -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <!-- 单元测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>
    </dependencies>

 2.2.4.创建实体类,并配置映射关系

注意引入的是:import javax.persistence.*,不是hibernate下面的

@Data
@Entity//告诉jpa这是一个实体类,需要把它跟数据库中的表做映射
@Table(name = "article") //建立实体类和表的映射关系
public class Article implements Serializable {
    @Id//声明当前私有属性为主键
    //指定主键生成策略,GenerationType.IDENTITY就是对应到mysq1中的数据自增策略
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer aid;
    //声明类的属性跟数据表字段的对应关系,如果属性名称和字段名称一致,可省略
    @Column(name = "title")
    private String title;
    private String author;
    private Date createTime;

}

2.2.5.加入 JPA 的核心配置文件

在maven工程的resources路径下创建一个名为META-INF的文件夹,在文件夹下创建一个名为
persistence.xml的配置文件。注意: META-INF文件夹名称不能修改,persistence.xml文件名称不能改。

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
             http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">

    <!--持久化单元
    name 持久化单元的名称 唯一
    transaction-type 事务类型
    RESOURCE LOCAL 本地事务
    JTA 分布式事务-->
    <!--持久化单元-->
    <persistence-unit name="springdata" transaction-type="RESOURCE_LOCAL">
        <!--配置 JPA 规范的服务提供商 -->
        <!--当项目中只有一个JPA的实现时,此选项可省略-->
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <!--指定实体类,此选项可省略-->
        <class>com.fang.domain.Article</class>
        <properties>
            <!-- 数据库驱动 -->
            <property name="javax.persistence.jdbc.driver"
                      value="com.mysql.cj.jdbc.Driver"/>
            <!-- 数据库地址 -->
            <property name="javax.persistence.jdbc.url"
                      value="jdbc:mysql://localhost:3306/springbootjpa?serverTimezone=Asia/Shanghai"/>
            <!-- 数据库用户名 -->
            <property name="javax.persistence.jdbc.user" value="root"/>
            <!-- 数据库密码 -->
            <property name="javax.persistence.jdbc.password"
                      value="123456"/>
            <!--jpa的核心配置中兼容hibernate的配置-->
            <!-- 是否显示SQL-->
            <property name="hibernate.show_sql" value="true"/>
            <!--是否格式化显示的SQL-->
            <property name="hibernate.format_sql" value="true"/>
            <!--自动建表
            update 如果数据库存在数据表,就使用:不存在,就创建
            create 小管数据库有没有数据表,每次SQL请求都会重新建表
            -->
            <property name="hibernate.hbm2ddl.auto" value="update"/>
        </properties>
    </persistence-unit>
</persistence>

2.2.6.测试添加

@Test
public void testSave() {
    // 创建文章对象
    Article article = new Article();
    article.setTitle("测试文章");
    article.setAuthor("黑马");
    article.setCreateTime(new Date());

    //1 创建持久化管理器工厂
    String persistenceUnitName = "springdata";
    EntityManagerFactory factory =
            Persistence.createEntityManagerFactory(persistenceUnitName);
    //2 创建持久化管理器(这个API是我们最重要的一个API,基于此API我们可以完成获取事务以及对数据库的cRUD操作]
    EntityManager entityManager = factory.createEntityManager();
    //3 获取事务,并开启
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    //4 操作
    entityManager.persist(article);
    //5 事务提交
    transaction.commit();
    //6 关闭资源
    entityManager.close();
}

2.2.7.测试查询

@Test
public void testFind() {
    //1 创建持久化管理器工厂
    String persistenceUnitName = "springdata";
    EntityManagerFactory factory =
            Persistence.createEntityManagerFactory(persistenceUnitName);
    //2 创建持久化管理器(这个API是我们最重要的一个API,基于此API我们可以完成获取事务以及对数据库的cRUD操作]
    EntityManager entityManager = factory.createEntityManager();
    //3 获取事务,并开启
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    //4 操作
    Article article = entityManager.find(Article.class, 1);
    System.out.println(article.toString());
    //5 事务提交
    transaction.commit();
    //6 关闭资源
    entityManager.close();
}

2.2.8.测试更新

@Test
public void testUpdate() {
    EntityManagerFactory factory =
            Persistence.createEntityManagerFactory("springdata");
    EntityManager entityManager = factory.createEntityManager();
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    // 4 操作:JPA的修改操作,要求必须先查询,再修改
    Article article = entityManager.find(Article.class, 1);
    //修改
    article.setAuthor("黑马程序员");
    entityManager.merge(article);
    transaction.commit();
    entityManager.close();
}

2.2.9.测试删除

@Test
public void testDelete() {
    EntityManagerFactory factory =
            Persistence.createEntityManagerFactory("springdata");
    EntityManager entityManager = factory.createEntityManager();
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    // 4 操作:JPA的修改操作,要求必须先查询,再删除
    Article article = entityManager.find(Article.class, 1);
    //删除
    entityManager.remove(article);
    transaction.commit();
    entityManager.close();
}

2.3.JPA的重要API介绍

  • Persistence:通过读取持久化单元名称,根据配置创建持久化管理器工厂
  • EntityTransaction:进行事务管理 begin commit rollback
  • EntityManagerFactory:这是一个工厂类,目的是为了创建Entitymanager,是一个线程安全的对象,对于这种工厂类,它的创建和销毁是十分耗费资源的,在一共工程中一般维持一个就好(单例)
  • EntityManager:这是我们最重要的一个API证,我们基于这个API可以完成数据库的CRUD操作,他是线程不安全的,需要保持线程独有。persist(T)find(Class,T)merge(class)remove(T)

3.SpringData JPA基础

3.1.SpringData JPA简介

SpringData JPA是Spring Data家族的一个成员,是Spring Data对JPA封装之后的产物,目的在于简化基于JPA的数据访问技术。使用SpringData JPA技术之后,开发者只需要声明Dao层的接口,不必再写实现类或其它代码,剩下的一切交给SpringData JPA来搞定 。

3.2.SpringData JPA快速入门

3.2.1.目标

本章节我们是实现的功能是搭建SpringData JPA环境(Spring+JPA),并实现一条数据的增删改查。

3.2.2.准备数据环境

同 2.2.2,也可让JPA自动生成表结构

3.2.3.创建java工程,导入坐标

<dependencies>
    <!--Jpa的支撑框架hibernate-->
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-c3p0</artifactId>
        <version>5.6.15.Final</version>
    </dependency>
    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.16</version>
    </dependency>
    <!-- 日志 -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.12</version>
    </dependency>
    <!-- 单元测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.28</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-jpa</artifactId>
        <version>2.3.9.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.15.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.2.15.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>5.2.15.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.13</version>
    </dependency>

</dependencies>

3.2.4.创建实体类

同2.2.4

3.2.5.编写dao接口

使用 Spring Data JPA操作数据库,只需要按照框架的规范提供 dao 接口,不需要提供在接口中定
义方法,也不需要为接口提供实现类就能完成基本的数据库的增删改查等功能。

 在 Spring Data JPA 中,对于定义符合规范的 Dao 层接口,我们只需要遵循以下几点就可以了:
1. 创建一个 Dao 层接口,并实现 JpaRepository 和 JpaSpecificationExecutor
2. 提供相应的泛型

  • JpaRepository<实体类类型,主键类型>:用来完成基本 CRUD 操作
  • JpaSpecificationExecutor<实体类类型>:用于复杂查询(分页等查询操作)
public interface ArticleDao extends JpaRepository<Article, Integer>,
        JpaSpecificationExecutor<Article> {
}

3.2.6.添加Spring整合Jpa的配置文件

resources目录下创建 applicationContext-jpa.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

    <!-- 1.配置包扫描,使注解生效-->
    <context:component-scan base-package="com.fang"/>

    <!-- 2.dataSource 配置一个数据源-->
    <bean id="dataSource"
          class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/springbootjpa?serverTimezone=Asia/Shanghai"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>
    <!-- 3.配置EntityManagerFactory,可以产生entityManager-->
    <bean id="entityManagerFactory"
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <!-- 注入数据源 -->
        <property name="dataSource" ref="dataSource"/>
        <!-- 指定实体类所在的包 -->
        <property name="packagesToScan" value="com.fang.domain"/>
        <!-- 指定jpa的实现提供者 -->
        <property name="persistenceProvider">
            <bean class="org.hibernate.jpa.HibernatePersistenceProvider"/>
        </property>
        <!--SpringData Jpa 兼容Hibernate使用 -->
        <property name="jpaVendorAdapter">
            <bean
                    class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 是否生成DDL语句 是否自动建表 -->
                <property name="generateDdl" value="true"/>
                <!-- 数据库厂商名称 -->
                <property name="database" value="MYSQL"/>
                <!-- 数据库方言 ,添加该属性导致建表失败-->
<!--                <property name="databasePlatform"  value="org.hibernate.dialect.MySQLDialect"/>-->
                <!-- 是否显示SQL -->
                <property name="showSql" value="true"/>
            </bean>
        </property>
    </bean>
    <!-- 4.事务管理器 -->
    <bean id="transactionManager"
          class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
<!--        <property name="dataSource" ref="dataSource"/>-->
    </bean>
    <!-- 整合spring data jpa -->
    <!--spring 通过代理的方式为dao接口提供实现类,需要指明为哪些接口去产生代理类-->
    <jpa:repositories base-package="com.fang.dao"
                      transaction-manager-ref="transactionManager"
                      entity-manager-factory-ref="entityManagerFactory"/>
</beans>

3.2.7.测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-jpa.xml")
public class springDataJpaTest {

    @Autowired
    private ArticleDao articleDao;
    //新增
    @Test
    public void testSave() {
        Article article = new Article();
        article.setTitle("测试文章111");
        article.setAuthor("黑马111");
        article.setCreateTime(new Date());
        articleDao.save(article);
    }

    //根据Id查询
    @Test
    public void testFindByAid() {
        // Optional,jdk1.8新特性
        Optional<Article> optional = articleDao.findById(1);
        System.out.println(optional.get());
    }
    //查询所有
    @Test
    public void testFindAll() {
        List<Article> list = articleDao.findAll();
        for (Article article : list) {
            System.out.println(article);
        }
    }

    //修改
    //spingdata Jpa的保存和修改使用的都是save方法
    //关键来看传入的实体是否有主键
    //---如果有主键,代表要修改
    //---如果没有主键,代表要保存
    @Test
    public void testUpdate() {
        Article article = new Article();
        article.setTitle("测试文章66");
        article.setAuthor("黑马3");
        article.setAid(1);
        articleDao.save(article);
    }
    //删除
    @Test
    public void testdelete() {
        articleDao.deleteById(1);
    }
}

3.3.SpringData Jpa运行原理分析

3.3.1.SpringData中的几个重要接口

Repository  标记接口,做标记,接口的子接口的实现类对象可以自动被springioc容器所识别,此接口的子接口中可以定义一些指定规范的方法
    |
    |
CrudRepository 定义了一下基本的CRUD方法
    |
    |
PagingAndsortingRepository 定义了排序和分页相关的查询方法
    |
    |
JpaRepository    重写了一下基本测CRUD方法
    |
    |
ArticleDao    自己定义的接口

通过上面的继承关系,我们可以看到我们自定义的接口ArticleDao继承了一系列的Repository口,而每一个接口都会给我们提供一部分的功能,这样继承下来,我们的ArticleDao不用任何的方法声明就拥有了很多的功能了。

3.3.2.SpringData Jpa底层运行原理

思考一个问题:我们找到了定义方法的接口,但并没有看到实现类,没有实现来就无法创建对象,那么真正干活的实现类到底在哪,它又是如何产生对象的呢?
下面我们通过debug的形式,寻找答案:

1.在运行时,Spring会使用JdkDynamicAopProxy为dao接口生成一个代理对象

2.那么这个代理对象是根据那个类代理出来的呢?点击进入JdkDynamicAopProxy源码查看invoke方法,发现targetSource代理的是SimpleJpaRepository类

3.通过对SimpleJpaRepository中代码的分析,我们看到最终执行保存的是EntityManager对象

总结:使用SpringData JPA开发底层还是用的JPA的API,SpringData JPA只是对标准 JPA 操作进行了进一步封装,已达到简化了Dao层代码开发的目的。

3.3.3.SpringData Jpa 与 Jpa 及 Hibernate的关系

3.3.4.SpringData Jpa 增删改操作详解

1.保存、修改

  • save(S entity):保存一个实体
  • saveAndFlush(S entity):保存一个实体,并且立即刷新缓存
  • saveAll(Iterable<S> entities):保存多个实体

可以看到 saveAll 方法可以保存 Iterable 类型数据,Collection 是它子类也可以被保存

 

//保存
@Test
public void testSave() {
    Article article = new Article();
    article.setTitle("汽车总动员1");
    article.setAuthor("汽车2");
    article.setCreateTime(new Date());

    //保存一个实体
    articleDao.save(article);

    //保存一个实体,并且立即刷新缓存
    //articleDao.saveAndFlush(article);
}

//保存多个
@Test
public void testSaveAll() {
    Article article1 = new Article();
    article1.setTitle("汽车总动员1");
    article1.setAuthor("汽车1");
    article1.setCreateTime(new Date());

    Article article2 = new Article();
    article2.setTitle("汽车总动员2");
    article2.setAuthor("汽车2");
    article2.setCreateTime(new Date());

    Article article3 = new Article();
    article3.setTitle("汽车总动员3");
    article3.setAuthor("汽车3");
    article3.setCreateTime(new Date());

    List list = new ArrayList();
    list.add(article1);
    list.add(article2);
    list.add(article3);

    //保存多个实体
    articleDao.saveAll(list);
}

2.删除

  • deleteById(ID id):根据主键删除
  • delete(T entity):根据实体删除,但是这个实体必须要有主键
  • deleteAll():删除所有  先查询--再一条条的删除
  • deleteAllInBatch():删除所有(不用查询)  一条语句删除所有记录
  • deleteAll(Iterable<? extends T> entities):先一条条的查,然后再一条条的删除
  • deleteInBatch(Iterable<T> entities):批量删除指定数据(不用查)  一条语句删除所有记录
//删除
@Test
public void testDeleteOne(){

    //1  根据主键删除
    //articleDao.deleteById(13);

    //2 根据实体删除,但是这个实体必须要有主键
    Article article = new Article();
    article.setAid(6);
    articleDao.delete(article);
}


//删除
@Test
public void testDeleteAll(){
    //1 删除所有  先查询--再一条条的删除
    //articleDao.deleteAll();

    //2 删除所有  一下子删除所有记录
    //articleDao.deleteAllInBatch();

    Article article1 = new Article();
    article1.setAid(13);

    Article article2 = new Article();
    article2.setAid(14);

    List list = new ArrayList();
    list.add(article1);
    list.add(article2);

    //3 批量删除指定数据  一条语句搞定
    articleDao.deleteInBatch(list);

    //4 先一条条的查,然后再一条条的删除
    //articleDao.deleteAll(list);
}

4.SpringData Jpa 多种查询方式

4.1.父接口方法查询

我们自定义的Dao接口可以使用它的父接口提供的方法,可以使用的方法如下图所示。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-jpa.xml")
public class Query1Test {

    @Autowired
    private ArticleDao articleDao;

    //根据主键查询
    @Test
    public void testFindById() {
        //根据一个主键查询
        Optional<Article> optional = articleDao.findById(21);
        System.out.println(optional.get());

        //根据多个主键查询,一条sql
        List<Integer> list = new ArrayList<>();
        list.add(21);
        list.add(23);
        list.add(25);
        List<Article> articles = articleDao.findAllById(list);
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    //查询所有
    @Test
    public void testFindAll() {
        List<Article> articles = articleDao.findAll();
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    //查询所有--排序
    @Test
    public void testFindAllWithSort() {
        //按照aid倒序排列
        Sort sort = Sort.by(Sort.Order.desc("aid"));
        List<Article> articles = articleDao.findAll(sort);
        for (Article article : articles) {
            System.out.println(article);
        }
    }


    //查询所有--分页
    @Test
    public void testFindAllWithPage() {
        //处理分页条件
        //page   当前是第几页(从0开始)    size  每页大小
        Pageable pageable = PageRequest.of(0, 2);
        Page<Article> page = articleDao.findAll(pageable);

        //总记录数  总页数  每页多少
        System.out.println("总记录数:" + page.getTotalElements());
        System.out.println("总页数:" + page.getTotalPages());
        System.out.println("每页多少:" + page.getSize());
        //当前页的元素
        List<Article> content = page.getContent();
        for (Article article : content) {
            System.out.println(article);
        }
    }


    //查询所有--分页+排序
    @Test
    public void testFindAllWithPageAndPage() {
        //按照aid倒序排列
        Sort sort = Sort.by(Sort.Order.desc("aid"));

        //处理分页条件
        //page   当前是第几页(从0开始)    size  每页大小
        Pageable pageable = PageRequest.of(0, 2, sort);
        Page<Article> page = articleDao.findAll(pageable);

        //总记录数  总页数  每页多少
        System.out.println("总记录数:" + page.getTotalElements());
        System.out.println("总页数:" + page.getTotalPages());
        System.out.println("每页多少:" + page.getSize());
        //当前页的元素
        List<Article> content = page.getContent();
        for (Article article : content) {
            System.out.println(article);
        }
    }

}

4.2.方法命名规则查询

顾名思义,方法命名规则查询就是根据方法的名字,就能创建查询。只需要按照SpringData JPA提供的方法命名规则定义方法的名称,就可以完成查询工作

SpringData JPA在程序执行的时候会根据方法名称进行解析,并自动生成查询语句进行查询按照SpringData JPA定义的规则,查询方法以findBy开头,涉及条件查询时,条件的属性用条件关键字连接,要注意的是:条件属性首字母需大写。框架在进行方法名解析时,会先把方法名多余的前缀截取掉,然后对剩下部分进行解析。

public interface ArticleDao extends JpaRepository<Article, Integer>,
        JpaSpecificationExecutor<Article> {

    //根据标题查询
    List<Article> findByTitle(String title);

    //根据标题模糊查询
    List<Article> findByTitleLike(String title);

    //根据标题和作者查询
    List<Article> findByTitleAndAuthor(String title, String author);

    //根据ID范围查询 > < between in
    List<Article> findByAidIsLessThan(Integer aid);

    List<Article> findByAidBetween(Integer startAid, Integer endAid);

    List<Article> findByAidIn(List<Integer> aids);

    //根据创建时间之后查询
    List<Article> findByCreateTimeAfter(Date createTime);

}

对应测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-jpa.xml")
public class Query2Test {
    
    @Autowired
    private ArticleDao articleDao;

    @Test
    public void testFindByTitle() {
        List<Article> articles = articleDao.findByTitle("汽车总动员1");
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    @Test
    public void testFindByTitleLike() {
        List<Article> articles = articleDao.findByTitleLike("%汽车总动员%");
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    @Test
    public void testFindByTitleAndAuthor() {
        List<Article> articles = articleDao.findByTitleAndAuthor("汽车总动员1", "汽车");
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    @Test
    public void testFindByAidIsLessThan() {
        List<Article> articles = articleDao.findByAidIsLessThan(18);
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    @Test
    public void testFindByAidBetween() {
        List<Article> articles = articleDao.findByAidBetween(20, 22);
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    @Test
    public void testFindByAidIn() {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(20);
        list.add(22);
        List<Article> articles = articleDao.findByAidIn(list);
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    @Test
    public void testFindByCreateTimeAfter() {
        List<Article> articles = articleDao.findByCreateTimeAfter(new Date());
        for (Article article : articles) {
            System.out.println(article);
        }
    }
}
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ? 2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or
x.firstname = ?2
Is,EqualsfindByFirstnameIs,
findByFirstnameEquals
… where x.firstname = ?1
BetweenfindByStartDateBetween… where x.startDate between ?1
and ?2
LessThanfindByAgeLessThan… where x.age < ?1
LessThanEqualfindByAgeLessThanEqual… where x.age ⇐ ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
AfterfindByStartDateAfter… where x.startDate > ?1
BeforefindByStartDateBefore… where x.startDate < ?1
IsNullfindByAgeIsNull… where x.age is null
IsNotNull,NotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
StartingWithfindByFirstnameStartingWith… where x.firstname like ?1
(parameter bound with
appended %)
EndingWithfindByFirstnameEndingWith… where x.firstname like ?1
(parameter bound with
prepended %)
ContainingfindByFirstnameContaining… where x.firstname like ?1
(parameter bound wrapped in
%)
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by
x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection age)… where x.age not in ?1
TRUEfindByActiveTrue()… where x.active = true
FALSEfindByActiveFalse()… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstame) = U

4.3.JPQL查询

4.3.1.方法命名规则查询的局限性

1.查询条件过多,where 后面条件10个8个,会导致方法名过长

2.传参需要一个个传,不支持一个实体对象来传
支持:findByAidBetween(Integer startAid, Integer endAid)
不支持:findByAidBetween(Article article)

4.3.2.JPQL查询

        使用SpringData JPA提供的查询方法已经可以解决大部分的应用场景,但是对于某些业务来说,我们还需要灵活的构造查询条件,这时就可以使用@Query注解,结合JPQL的语句方式完成查询。
        JPQL,全称是Java Persistence Query Language。JPQL语句是JPA中定义的一种查询语言,此种语言的用意是让开发者忽略数据库表和表中的字段,而关注实体类及实体类中的属性。
它的写法十分类似于SQL语句的写法,但是要把查询的表名换成实体类名称,把表中的字段名换成实体类的属性名称。

/**
 * JpaRepository<实体类类型,主键类型>:用来完成基本 CRUD 操作
 * JpaSpecificationExecutor<实体类类型>:用于复杂查询(分页等查询操作)
 */
public interface ArticleDao extends JpaRepository<Article, Integer>,
        JpaSpecificationExecutor<Article> {

    //JPQL:类似于SQL语句,但是要使用实体类名代替表名,使用属性名代替字段名[面向对象查询]
    //展示位置参数绑定[按照title和author查询]
    //占位符从1开始
    @Query("from Article a where a.title = ?1 and a.author = ?2")
    List<Article> findByCondition1(String title,String author);

    //展示方法参数绑定查询中参数
    //推荐使用这种
    @Query("from Article a where a.title = :title and a.author = :authors")
    List<Article> findByCondition2(@Param("title")String title, @Param("authors") String author);

    //展示like模糊查询
    @Query("from Article a where a.title like %:title% ")
    List<Article> findByCondition3(@Param("title")String title);

    //展示排序查询
    @Query("from Article a where a.title like %:title% order by a.aid desc")
    List<Article> findByCondition4(@Param("title")String title);

    //展示分页查询
    @Query("from Article a where a.title like %:title%")
    List<Article> findByCondition5(Pageable pageable, @Param("title")String title);

    //展示传入集合参数查询
    @Query("from Article a where a.aid in :aids")
    List<Article> findByCondition6(@Param("aids") List<Integer> aids);

    //展示传入Bean进行查询(SPEL表达式查询)
    @Query("from Article a where a.author = :#{#article.author} and a.title = :#{#article.title}")
    List<Article> findByCondition7(@Param("article") Article author);
}

对应测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-jpa.xml")
public class Query3Test {

    @Autowired
    private ArticleDao articleDao;

    @Test
    public void testFindByCondition1() {
        List<Article> articles = articleDao.findByCondition1("汽车总动员1","汽车1");
        for (Article article : articles) {
            System.out.println(article);
        }
    }
    @Test
    public void testFindByCondition2() {
        List<Article> articles = articleDao.findByCondition2("汽车总动员1","汽车1");
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    @Test
    public void testFindByCondition3() {
        List<Article> articles = articleDao.findByCondition3("汽车总动员1");
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    @Test
    public void testFindByCondition4() {
        List<Article> articles = articleDao.findByCondition4("汽车总动员1");
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    @Test
    public void testFindByCondition5() {
        Pageable pageable = PageRequest.of(0,2);
        List<Article> articles = articleDao.findByCondition5(pageable,"汽车总动员1");
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    @Test
    public void testFindByCondition6() {
        List<Integer> ids = new ArrayList<>();
        ids.add(22);
        ids.add(21);
        List<Article> articles = articleDao.findByCondition6(ids);
        for (Article article : articles) {
            System.out.println(article);
        }
    }

    @Test
    public void testFindByCondition7() {
        Article article = new Article();
        article.setAuthor("汽车1");
        article.setTitle("汽车总动员1");
        List<Article> articles = articleDao.findByCondition7(article);
        for (Article article1 : articles) {
            System.out.println(article1);
        }
    }
}

4.4.本地SQL查询

基本不会使用,除非是出现非常复杂的业务情况导致SQL非常复杂,JPQL搞不定的时候

    //本地SQL查询:使用自己写的sql
    @Query(value = "select * from article a where a.title = ?1 and a.author =?2", nativeQuery = true)
    List<Article> findByCondition8(String title, String author);

对应测试类

    @Test
    public void testFindByCondition8() {
        List<Article> articles = articleDao.findByCondition8("汽车总动员1", "汽车");
        for (Article article : articles) {
            System.out.println(article);
        }
    }

4.5.动态查询

问题:根据查询条件进行动态查询

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

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-jpa.xml")
public class Query4Test {

    @Autowired
    private ArticleDao articleDao;

    //按照标题和作者进行查询,以不为空的属性作为查询条件
    @Test
    public void testFindAll() {
        String title = "汽车总动员1";
        String author ="";
        List<Article> articles = articleDao.findAll(new Specification<Article>() {
            /**
             * @param root  代表实体对象,我们可以通过它获取属性值
             * @param cq    用于生成SQL语句
             * @param cb    用于拼接查询条件
             * @return
             */
            @Override
            public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
                ArrayList<Predicate> list = new ArrayList<>();
                if (!StringUtils.isEmpty(title)) {
                    //拼接作为查询条件
                    Predicate predicate = cb.equal(root.get("title").as(String.class), title);
                    list.add(predicate);
                }

                if (!StringUtils.isEmpty(author)) {
                    //拼接作为查询条件
                    Predicate predicate = cb.equal(root.get("author").as(String.class), author);
                    list.add(predicate);
                }
                return cb.and(list.toArray(new Predicate[]{}));
            }
        });

        for (Article article:articles){
            System.out.println(article);
        }
    }


    //按照标题、作者、分页进行查询,以不为空的属性作为查询条件
    @Test
    public void testFindAllWithPage() {
        String title = "";
        String author ="";
        Pageable pageable = PageRequest.of(0,3);
        Page<Article> articles = articleDao.findAll(new Specification<Article>() {
            /**
             * @param root  代表实体对象,我们可以通过它获取属性值
             * @param cq    用于生成SQL语句
             * @param cb    用于拼接查询条件
             * @return
             */
            @Override
            public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
                ArrayList<Predicate> list = new ArrayList<>();
                if (!StringUtils.isEmpty(title)) {
                    //拼接作为查询条件
                    Predicate predicate = cb.equal(root.get("title").as(String.class), title);
                    list.add(predicate);
                }

                if (!StringUtils.isEmpty(author)) {
                    //拼接作为查询条件
                    Predicate predicate = cb.equal(root.get("author").as(String.class), author);
                    list.add(predicate);
                }
                return cb.and(list.toArray(new Predicate[]{}));
            }
        }, pageable);

        for (Article article:articles){
            System.out.println(article);
        }
    }

    //按照标题、作者、分页排序进行查询,以不为空的属性作为查询条件
    @Test
    public void testFindAllWithPageAndSort() {
        String title = "";
        String author ="";
        Pageable pageable = PageRequest.of(0,3, Sort.by(Sort.Order.desc("aid")));
        Page<Article> articles = articleDao.findAll(new Specification<Article>() {
            /**
             * @param root  代表实体对象,我们可以通过它获取属性值
             * @param cq    用于生成SQL语句
             * @param cb    用于拼接查询条件
             * @return
             */
            @Override
            public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
                ArrayList<Predicate> list = new ArrayList<>();
                if (!StringUtils.isEmpty(title)) {
                    //拼接作为查询条件
                    Predicate predicate = cb.equal(root.get("title").as(String.class), title);
                    list.add(predicate);
                }

                if (!StringUtils.isEmpty(author)) {
                    //拼接作为查询条件
                    Predicate predicate = cb.equal(root.get("author").as(String.class), author);
                    list.add(predicate);
                }
                return cb.and(list.toArray(new Predicate[]{}));
            }
        }, pageable);

        for (Article article:articles){
            System.out.println(article);
        }
    }

}

5.SpringData JPA实现多表操作

5.1.多表关系分析

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

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

  • 第一步:首先确定两张表之间的关系
  • 第二步:在实体类中描述出两个实体的关系
  • 第三步:配置出实体类和数据库表的关系映射(重点)

5.2.案例表间关系

5.3.一对一关系

5.3.1.数据环境(article和article_data的一对一关系)

5.3.2.创建实体类,并在类中配置表间关系

1.创建文章类

@Data
@Entity//告诉jpa这是一个实体类,需要把它跟数据库中的表做映射
@Table(name = "article") //建立实体类和表的映射关系
public class Article implements Serializable {
    @Id//声明当前私有属性为主键
    //指定主键生成策略,GenerationType.IDENTITY就是对应到mysq1中的数据自增策略
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer aid;
    //声明类的属性跟数据表字段的对应关系,如果属性名称和字段名称一致,可省略
    @Column(name = "title")
    private String title;
    private String author;
    private Date createTime;


    //1 声明类间关系
    //声明主动放弃关系维护 mappedBy="当前类在对方类中的属性名"
    //cascade 级联操作,当保存Article的时候,同时保存ArticleData
    @OneToOne(mappedBy = "article", cascade = CascadeType.PERSIST)
    private ArticleData articleData;

}

2.创建文章详情类 

@Data
@Entity
@Table(name= "article_data")
public class ArticleData implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String content;

    //让这个实体维护关系
    //name                    当前表中的外键名
    //referencedColumnName    指向的对方表中的主键名
    @OneToOne
    @JoinColumn(name="articleId",referencedColumnName="aid",unique=true)
    private Article article;
    
}

5.3.3.添加ArticleDao接口

/**
 * JpaRepository<实体类类型,主键类型>:用来完成基本 CRUD 操作
 * JpaSpecificationExecutor<实体类类型>:用于复杂查询(分页等查询操作)
 */
public interface ArticleDao extends JpaRepository<Article, Integer>,
        JpaSpecificationExecutor<Article> {

}

 5.3.4.测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-jpa.xml")
public class One2OneTest {

    @Autowired
    private ArticleDao articleDao;
    //查询所有
    @Test
    public void testSave() {
        //创建文章对象
        Article article = new Article();
        article.setTitle("比亚迪海豹");
        article.setAuthor("汽车之家");
        article.setCreateTime(new Date());

        //创建文章内容对象
        ArticleData articleData = new ArticleData();
        articleData.setContent("比亚迪海豹你值得拥有");

        // 建立两个对象间的关系
        article.setArticleData(articleData);
        articleData.setArticle(article);

        articleDao.save(article);
    }
    
}

5.4.一对多关系

5.4.1.数据环境(article和comment的一对多关系)

5.4.2.创建实体类,并在类中配置表间关系

JPA和lombok整合报错,栈溢出 java.lang.StackOverflowError,将@Data变成@Getter,@Setter,重写toString方法解决方案

1.修改文章类,添加文章跟评论的映射 

@Getter
@Setter
@ToString
@Entity//告诉jpa这是一个实体类,需要把它跟数据库中的表做映射
@Table(name = "article") //建立实体类和表的映射关系
public class Article implements Serializable {
    @Id//声明当前私有属性为主键
    //指定主键生成策略,GenerationType.IDENTITY就是对应到mysq1中的数据自增策略
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer aid;
    //声明类的属性跟数据表字段的对应关系,如果属性名称和字段名称一致,可省略
    @Column(name = "title")
    private String title;
    private String author;
    private Date createTime;

    //1 声明类间关系
    //声明主动放弃关系维护 mappedBy="当前类在对方类中的属性名"
    //cascade 级联操作,当保存Article的时候,同时保存ArticleData
    @OneToOne(mappedBy = "article", cascade = CascadeType.PERSIST)
    private ArticleData articleData;

    //2 在类中使用注解再声明表间关系
    // --书写注解
    // --明确谁来维护关系(在多的一方维护关系)
    //  ----在维护的一方主动声明维护策略,在不维护的一方声明主动放弃
    @OneToMany(mappedBy = "article")
    private Set<Comment> comments = new HashSet<>();
    
}

2.创建文章评论类

@Getter
@Setter
@ToString
@Entity
@Table(name="comment")
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer cid;

    private String comment;
    @ManyToOne()
    @JoinColumn(name="aid",referencedColumnName = "aid")
    private Article article;
    
}

5.4.3.添加CommentDao接口

/**
 * JpaRepository<实体类类型,主键类型>:用来完成基本 CRUD 操作
 * JpaSpecificationExecutor<实体类类型>:用于复杂查询(分页等查询操作)
 */
public interface CommentDao extends JpaRepository<Comment, Integer>,
        JpaSpecificationExecutor<Comment> {

}

5.4.4.测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-jpa.xml")
public class One2ManyTest {

    @Autowired
    private ArticleDao articleDao;

    @Autowired
    private CommentDao commentDao;

    //查询所有
    @Test
    public void testSave() {
        //创建文章对象
        Article article = new Article();
        article.setTitle("比亚迪海豹666  ");
        article.setAuthor("汽车之家");
        article.setCreateTime(new Date());

        //创建文章评论对象
        Comment comment1 = new Comment();
        comment1.setComment("不错,不错");

        Comment comment2 = new Comment();
        comment2.setComment("想买想买!");

        // 建立两个对象间的关系
        comment1.setArticle(article);
        comment2.setArticle(article);

        Set<Comment> comments = new HashSet<>();
        comments.add(comment1);
        comments.add(comment2);
        article.setComments(comments);

        // 保存操作
        articleDao.save(article);
        commentDao.save(comment1);
        commentDao.save(comment2);
    }

}

5.5.多对多关系

5.5.1.数据环境(article跟type之间的多对多关系)

 5.5.2.创建实体类,并在类中配置表间关系

1.修改文章类,添加文章跟评论用户的多对多关系

@Getter
@Setter
@ToString
@Entity//告诉jpa这是一个实体类,需要把它跟数据库中的表做映射
@Table(name = "article") //建立实体类和表的映射关系
public class Article implements Serializable {
    @Id//声明当前私有属性为主键
    //指定主键生成策略,GenerationType.IDENTITY就是对应到mysq1中的数据自增策略
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer aid;
    //声明类的属性跟数据表字段的对应关系,如果属性名称和字段名称一致,可省略
    @Column(name = "title")
    private String title;
    private String author;
    private Date createTime;

    //1 声明类间关系
    //声明主动放弃关系维护 mappedBy="当前类在对方类中的属性名"
    //cascade 级联操作,当保存Article的时候,同时保存ArticleData
    @OneToOne(mappedBy = "article", cascade = CascadeType.PERSIST)
    private ArticleData articleData;

    //2 在类中使用注解再声明表间关系
    // --书写注解
    // --明确谁来维护关系(在多的一方维护关系)
    //  ----在维护的一方主动声明维护策略,在不维护的一方声明主动放弃
    @OneToMany(mappedBy = "article")
    private Set<Comment> comments = new HashSet<>();


    @ManyToMany(mappedBy = "articles")
    private Set<Type> types = new HashSet<>();

}

2.修改文章类,添加文章跟评论用户的多对多关系

@Setter
@Getter
@ToString
@Entity
@Table(name = "type")
public class Type {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Integer tid;
    private String name;

    @ManyToMany
    @JoinTable(
            //代表中间表名称
            name = "article_type",
            //中间表的外键对应到当前表的主键名称
            joinColumns={@JoinColumn(name = "tid",referencedColumnName = "tid")},
            //中间表的外键对应到对方表的主键名称
            inverseJoinColumns = {@JoinColumn(name = "aid",referencedColumnName = "aid")}
    )
    private Set<Article> articles = new HashSet<>();

}

5.5.3.添加TypeDao接口

public interface TypeDao extends JpaRepository<Type, Integer>,
        JpaSpecificationExecutor<Type> {

}

5.5.4.测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-jpa.xml")
public class Many2ManyTest {

    @Autowired
    private ArticleDao articleDao;

    @Autowired
    private TypeDao typeDao;

    //查询所有
    @Test
    public void testSave() {
        //创建文章对象
        Article article1 = new Article();
        article1.setTitle("比亚迪海豹777");
        article1.setAuthor("汽车之家777");
        article1.setCreateTime(new Date());

        Article article2 = new Article();
        article2.setTitle("比亚迪海豹888");
        article2.setAuthor("汽车之家888");
        article2.setCreateTime(new Date());

        //创建文章类型对象
        Type type1 = new Type();
        type1.setName("汽车");

        Type type2 = new Type();
        type2.setName("生活");

        // 建立两个对象间的关系
        Set<Type> types = new HashSet<>();
        types.add(type1);
        types.add(type2);
        article1.setTypes(types);
        article2.setTypes(types);

        Set<Article> articles = new HashSet<>();
        articles.add(article1);
        articles.add(article2);
        type1.setArticles(articles);
        type2.setArticles(articles);

        // 保存操作
        articleDao.save(article1);
        articleDao.save(article2);
        typeDao.save(type1);
        typeDao.save(type2);

    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值