Spring Data JPA介绍与Spring的整合

一、JPA 、Hibernate、Spring Data JPA与Spring Data简介

    Spring Data JPA官网介绍:https://spring.io/projects/spring-data-jpa

1、什么是JPA?

JPA(Java Persistence API)是Sun官方提出的Java持久化API规范。可以通过JDK 5.0注解或者XML描述【对象-关系表】之间的映射关系,并将实体对象持久化到数据库中。主要是为了简化现有的持久化开发工作和整合ORM技术,使得应用程序以统一的方式访问持久层。

2、什么是Hibernate?

Hibernate 是一个开源的全自动的ORM框架,它对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,Hibernate 是一个实现了 ORM 思想的框架,封装了 JDBC。 

3、什么是Spring Data JPA?

Spring Data JPA是Spring提供的一套简化JPA开发的框架,它是在JPA规范下提供了Repository层的实现,按照约定好的【方法命名规则】写dao层接口,就可以在不写接口实现的情况下,实现对数据库的访问和操作。同时提供了很多除了CRUD之外的功能,如分页、排序、复杂查询等等。

Spring Data JPA的功能实现默认使用的Hibernate,也可以使用其他持久层框架。

4、什么是Spring Data?

Spring Data项目是为了简化构建基于Spring框架应用的数据访问技术,是Spring官方提供的一套数据层的综合解决方案。(或者可以封装其他的持久层解决方案的一个解决方案)。它支持关系型数据库、非关系型数据库、Map-Reduce框架、云数据服务等。 

Spring Data 包含很多个子模块:

  • Commons - 提供共享的基础框架,适合各个子项目使用,支持跨数据库持久化
  • Hadoop - 基于 Spring 的 Hadoop 作业配置和一个 POJO 编程模型的 MapReduce 作业
  • Key-Value - 集成了 Redis 和 Riak ,提供多个常用场景下的简单封装
  • Document - 集成文档数据库:CouchDB 和 MongoDB 并提供基本的配置映射和资料库支持
  • Graph - 集成 Neo4j 提供强大的基于 POJO 的编程模型
  • Graph Roo AddOn - Roo support for Neo4j
  • JDBC Extensions - 支持 Oracle RAD、高级队列和高级数据类型
  • JPA - 简化创建 JPA 数据访问层和跨存储的持久层功能
  • Mapping - 基于 Grails 的提供对象映射框架,支持不同的数据库
  • Examples - 示例程序、文档和图数据库
  • Guidance - 高级文档

    

 Spring Data JPA是Spring Data的一个模块。

5、它们之间的关系

JPA是一套ORM规范接口,接口是需要实现才能工作。而 Hibernate就是实现了JPA接口的ORM框架。

Spring Data JPA 可以理解为 JPA 规范的再次封装抽象,底层默认还是使用了 Hibernate 对JPA 技术实现。

Spring Data JPA是Spring Data的一个模块。

按照时间线来说就明白了

  • 开发 Hibernate 的团队开发了 Hibernate
  • 制订 J2ee 规范的团队邀请 Hibernate 的核心在 Hibernate 基础上制订了 JPA (Java Persistent API)标准。从功能上看,JPA 是 Hibernate 的子集。
  • Spring 的团队使用 Spring 对 JPA 做了封装,就是 Spring Data JPA 了。

总之,JPA 是一个 API 标准,除了 Hibernate 外,还有其它厂商的实现,例如 Eclipse 的 TopLink。Spring Data Jpa 是个对 JPA 的封装,帮助程序员以 Spring 的方式来使用 JPA。

6、一些名词含义

ORM(Object Relational Mapping):通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。本质就是将关系和对象通过映射文件进行联系。我们在程序中只需要操作对象即可操作对应的数据库表的数据。

POJO(Plain Ordinary Java Object)简单的Java对象,实际就是普通JavaBeans。

HQL(Hibernate Query Language)是面向对象的查询。

JPQL(Java Persistence Query Language)查询语言,通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合。

JPA 规范提供了 JPQL ,Hibernate 提供了 HQL来查询数据库。JPQL的原型就是Hibernate 的HQL。

  

上面内容更多参考这两篇文章,整的明明白白

Hibernate和Spring Data JPA有什么区别?

JPA和SpringDataJPA简介

二、Spring与Spring Data JPA的整合

在实际的工程中,推荐采用 Spring Data JPA + ORM(如:Hibernate)进行开发,这样在切换不同的ORM提供了方面,同时也使得Repository变得简单。程序低耦合。

1、创建一个maven java项目,在 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.jq.springdatajpademo</groupId>
    <artifactId>springdatajpademo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <!-- spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.7.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.2.7.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.7.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.2.7.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.7.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>5.2.7.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.7.RELEASE</version>
        </dependency>
        <!-- slf4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
        <!-- hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.2.17.Final</version>
        </dependency>
        <!-- druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>
        <!-- spring-data-jpa -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>2.3.1.RELEASE</version>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.10.3</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310 -->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.10.3</version>
        </dependency>


        <!-- spring-test -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.7.RELEASE</version>
            <scope>test</scope>
        </dependency>

        <!-- junit  -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <extensions>true</extensions>
            </plugin>
        </plugins>
    </build>

</project>

2、jdbc.properties :

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/springdatajpa_demo?useUnicode=true&characterEncoding=utf8&useSSL=true
jdbc.username=root
jdbc.password=123456

druid.initialSize=5
druid.minIdle=5
druid.maxActive=20
druid.maxWait=5

hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
hibernate.show_sql=true
hibernate.format_sql=true
hibernate.hbm2ddl.auto=update

3、log4j.properties :

log4j.rootLogger = debug,stdout, D
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.Threshold = INFO
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p %m%n
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = ./log4j.log
log4j.appender.D.Append = true
log4j.appender.D.Threshold = DEBUG
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern=%d %p %m%n

4、spring.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:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       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/tx http://www.springframework.org/schema/tx/spring-tx.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/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

    <!-- 1. 导入源属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties" />
    <!-- c3p0 连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <!-- 类似EL表达式取资源文件的值 -->
        <property name="driverClassName" value="${jdbc.driverClassName}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="initialSize" value="${druid.initialSize}"></property>
        <property name="minIdle" value="${druid.minIdle}"></property>
        <property name="maxActive" value="${druid.maxActive}"></property>
        <property name="maxWait" value="${druid.maxWait}"></property>
    </bean>

    <!-- 2. 整合jpa, 配置Hibernate的Sessionfactory实例 -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <!-- 数据源 -->
        <property name="dataSource" ref="dataSource"></property>
        <!-- jpa的实现产品 -->
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"></bean>
        </property>
        <!-- 配置实体类所在的包 -->
        <property name="packagesToScan" value="cn.jq.springdatajpademo.model"></property>

        <!-- jpa的实现的基本属性 -->
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.dialect">${hibernate.dialect}</prop>
                <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
                <prop key="hibernate.format_sql">${hibernate.format_sql}</prop>
                <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
            </props>
        </property>
    </bean>

    <!-- 配置spring的jpa声明事务管理器 -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"></property>
    </bean>

    <!-- 开启事务注释 @Transactional -->
    <!--<tx:annotation-driven transaction-manager="transactionManager" /> -->

    <!--  配置事务通知 -->
    <tx:advice id="tsAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="load*" read-only="true"/>
            <tx:method name="find*" read-only="true"/>
            <tx:method name="select*" read-only="true"/>
            <tx:method name="*" read-only="false"/>
        </tx:attributes>
    </tx:advice>
    <!-- 配置事务切入点 -->
    <aop:config>
        <aop:pointcut expression="execution(* cn.jq.springdatajpademo.service.*.*(..))" id="txPointcut"/>
        <aop:advisor advice-ref="tsAdvice" pointcut-ref="txPointcut"/>
    </aop:config>

    <!-- 配置spring data jpa : base-package会扫描对应的包,把里面的接口的实现对象放到spring的ioc容器里-->
    <jpa:repositories base-package="cn.jq.springdatajpademo.dao" entity-manager-factory-ref="entityManagerFactory"></jpa:repositories>

        <context:component-scan base-package="cn.jq.springdatajpademo"></context:component-scan>

</beans>

5、pojo层

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.format.annotation.DateTimeFormat;

import javax.persistence.*;
import java.time.LocalDateTime;

/**
 * 用户的实体类
 *      配置映射关系
 *
 *   1.实体类和表的映射关系
 *      @Entity:声明实体类
 *      @Table : 配置实体类和表的映射关系
 *          name : 配置数据库表的名称
 *   2.实体类中属性和表中字段的映射关系
 *
 *
 */
@Entity
@Table(name = "t_user")
public class User {

    /**
     * @Id:声明主键的配置
     * @GeneratedValue:配置主键的生成策略
     *      strategy
     *          GenerationType.IDENTITY :自增,mysql
     *                 * 底层数据库必须支持自动增长(底层数据库支持的自动增长方式,对id自增)
     *          GenerationType.SEQUENCE : 序列,oracle
     *                 * 底层数据库必须支持序列
     *          GenerationType.TABLE : jpa提供的一种机制,通过一张数据库表的形式帮助我们完成主键自增
     *          GenerationType.AUTO : 由程序自动的帮助我们选择主键生成策略
     * @Column:配置属性和字段的映射关系
     *      name:数据库表中字段的名称
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;

    private Integer age;


    @JsonFormat(shape = JsonFormat.Shape.STRING, timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @Column(name = "create_time")
    private LocalDateTime createtime; /** 创建时间*/

    public Long getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getAge() {
        return age;
    }

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

    public LocalDateTime getCreatetime() {
        return createtime;
    }

    public void setCreatetime(LocalDateTime createtime) {
        this.createtime = createtime;
    }

    // 空构造
    public User() {
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", age=" + age +
                ", createtime=" + createtime +
                '}';
    }
}

6、dao层

import cn.jq.springdatajpademo.model.User;
import org.springframework.data.repository.Repository;

/**
 * 符合SpringDataJpa的dao层接口规范
 *      Repository<操作的实体类类型,实体类中主键属性的类型>
 *          * Repository标记接口,并没有提供任何操作方法
 */
public interface UserDao extends Repository<User, Long> {

    // 定义一个方法(方法名有一定的规则), 不需要做任何的sql实现
    User getById(Long id);
}

7、测试类

import cn.jq.springdatajpademo.dao.UserDao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @Description:
 * @Auther: leijq
 * @Date: 2020-07-06 22:54
 * @Version: V1.0
 */
@RunWith(SpringJUnit4ClassRunner.class) //声明spring提供的单元测试环境
@ContextConfiguration(locations = "classpath:spring.xml")//指定spring容器的配置信息
public class UserDaoTest {

    @Autowired
    private UserDao userDao;

    @Test
    public void testGetById() {
        // 先注释,运行UserDaoTest类,生成表,手动在表中添加一些记录,然后再测试testGetById
//        User user = userDao.getById(1L);
//        System.out.println(user);
    }
}

 

      

    

8、测试demo

1)模糊查询

Like需要自己写%,Containing不需要自己写%。

== UserDao == 
    List<User> findUsersByUsernameLike(String username);
    List<User> findUsersByUsernameContaining(String username);

== test == 
    @Test
    public void testFindUsersByUsernameLike(){
        List<User> users = userDao.findUsersByUsernameLike("%李%");
        System.out.println(users);
    }

    @Test
    public void testFindUsersByUsernameContaining(){
        List<User> users = userDao.findUsersByUsernameContaining("李");
        System.out.println(users);
    }

2)关联查询

新建一个Role类,User类做好关联,数据库添加点数据。关联对象和它的属性之间用 _ 连接

      

== UserDao == 
    List<User> getUsersByRole_Name(String name);

== test == 
    @Test
    public void testGetUsersByRole_Name() {
        List<User> users = userDao.getUsersByRole_Name("test");
        System.out.println(users);
    }

    

3)自定义JPQL查询语言通过 @query注解来实现

对于复杂的功能,比如,子查询等,需要我们自定义JPQL查询语言

(1)没参数的@query查询:查找age最大User记录

    @Query("select u from User u where u.age = (select max(age) from User)")
    List<User> listUserByMaxAge();

(2)带参数的@query查询,使用占位符:注意:参数的顺序必须一一对应

    @Query("select u from User u where u.username = ?1 and u.password = ?2")
    User getUser(String username, String password);

(3)带参数的@query查询,使用具名参数:注意:对参数顺序没要求,和@Param注解一起使用

    @Query("select u from User u where u.username = :username and u.password =:password")
    User getUser2(@Param("username") String username, @Param("password") String password);

(4)带参数的@query查询,使用like的时候,是可以用%的,

注意:hibernate和mybatis都是不可以使用的,springdata可以。

    @Query("select u from User u where u.username like %?1%")
    List<User> listByLike(String username);
    
    @Query("select u from User u where u.username like %:username%")
    List<User> listByLike2(@Param("username") String username);

(5)使用@query执行原生的sql查询:

    @Query(value = "select * from t_user where username like %:username%", nativeQuery = true)
    List<User> listByLike3(@Param("username") String username);

9、实现JPQL的修改和删除

在@Query注解的上面加上@Modifying注解,就可以执行update和delete的jpql语句。

== UserDao == 
    @Modifying
    @Query("update User u set u.age = ?1 where u.id = ?2")
    void updateAgeById(Integer age, Long id);

    @Modifying
    @Query("delete from  User u where u.id = ?1")
    void deleteById(Long id);

== test == 
    @Test
    public void test() {
        userService.updateAgeById(99, 1L);
        userService.deleteById(2L);
    }

注意:由于Springdata执行update和delete语句需要可写事务支持的,而上面整合使用声明式事务处理,事务是放在service层,所以必须在service层里调用它才可以,如果使用dao层调用会报错。

Springdata需要事务,那么为什么查询可以直接执行呢?默认情况下Springdata也是有事务的,只不过这个事务是只读事务,不能修改和删除数据库里的记录。另外还要注意一个问题,注解@Modifying是不能做insert语句的,jpql不支持! 

 

三、测试类的实现分析

1、dao层接口继承的Repository接口

查看 Repository接口的源码:该接口中没有定义任何方法,即称之为标记接口

这个标记接口的作用:是把继承它的子接口,比如:UserDao标记为 Repository Bean,Spring就会为这个接口创建代理实现类,并且放入到IOC反转容器里,就像以前自己写的 UserDao的实现类 UserDaoImpl一样。

2、extends Repository<User, Integer>继承的写法还可以使用一个 @RepositoryDefinition注解来替代

import org.springframework.data.repository.RepositoryDefinition;


@RepositoryDefinition(domainClass = User.class, idClass = Long.class)
public interface UserDao {

    // 定义一个方法(方法名有一定的规则), 不需要做任何的sql实现
    User getById(Long id);
}

3、在dao层的接口中定义方法

遵守一些规定:

1)查询的方法:find或get或read开头

2)查询条件:用关键字链接,涉及属性首写字母大写

3)支持级联查询,关联对象和它的属性之间用 _ 连接

KeywordSampleJPQL snippet

And

findByLastnameAndFirstname

… where x.lastname = ?1 and x.firstname = ?2

Or

findByLastnameOrFirstname

… where x.lastname = ?1 or x.firstname = ?2

IsEquals

findByFirstname,findByFirstnameIs,findByFirstnameEquals

… where x.firstname = ?1

Between

findByStartDateBetween

… where x.startDate between ?1 and ?2

LessThan

findByAgeLessThan

… where x.age < ?1

LessThanEqual

findByAgeLessThanEqual

… where x.age <= ?1

GreaterThan

findByAgeGreaterThan

… where x.age > ?1

GreaterThanEqual

findByAgeGreaterThanEqual

… where x.age >= ?1

After

findByStartDateAfter

… where x.startDate > ?1

Before

findByStartDateBefore

… where x.startDate < ?1

IsNullNull

findByAge(Is)Null

… where x.age is null

IsNotNullNotNull

findByAge(Is)NotNull

… where x.age not null

Like

findByFirstnameLike

… where x.firstname like ?1

NotLike

findByFirstnameNotLike

… where x.firstname not like ?1

StartingWith

findByFirstnameStartingWith

… where x.firstname like ?1 (parameter bound with appended %)

EndingWith

findByFirstnameEndingWith

… where x.firstname like ?1 (parameter bound with prepended %)

Containing

findByFirstnameContaining

… where x.firstname like ?1 (parameter bound wrapped in %)

OrderBy

findByAgeOrderByLastnameDesc

… where x.age = ?1 order by x.lastname desc

Not

findByLastnameNot

… where x.lastname <> ?1

In

findByAgeIn(Collection<Age> ages)

… where x.age in ?1

NotIn

findByAgeNotIn(Collection<Age> ages)

… where x.age not in ?1

True

findByActiveTrue()

… where x.active = true

False

findByActiveFalse()

… where x.active = false

IgnoreCase

findByFirstnameIgnoreCase

… where UPPER(x.firstame) = UPPER(?1)

4、Repository接口的子接口(重点内容)

    

 

到此,Spring Data JPA与Spring的简单使用搞定,对于Repository接口的子接口掌握的重点

 

—— Stay Hungry. Stay Foolish. 求知若饥,虚心若愚。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值