复习_Hibernate_一级缓存、Query对象、Criteria对象

1.Hibernate持久化类和对象标识符

1.1 持久化类的编写规范

1.什么是持久化类
Hibernate是持久层的ORM映射框架,专注于数据的持久化工作。所谓的持久化,就是将内存中的数据永久存储到关系型数据库中。
那么知道了什么是持久化,什么又是持久化类呢?其实所谓的持久化类指的是一个Java类与数据库表建立了映射关系,那么这个类称为是持久化类。
其实你可以简单的理解为持久化类就是一个Java类有了一个映射文件与数据库的表建立了关系。那么我们在编写持久化类的时候有哪些要求呢?接下来我们来看一下:

2.持久化类的编写规则
我们在编写持久化类的时候需要有以下几点需要注意:

  • 持久化类需要提供无参数的构造方法。因为在Hibernate的底层需要使用反射生成类的实例。
  • 持久化类的属性需要私有,对私有的属性提供公有的get和set方法。因为在Hibernate底层会将查询到的数据进行封装。
  • 持久化类的属性要尽量使用包装类的类型。因为包装类和基本数据类型的默认值不同,包装类的类型语义描述更清晰而基本数据类型不容易描述。
    举个例子:
    假设表中有一列员工工资,如果使用double类型,如果这个员工工资忘记录入到系统中,系统会将默认值0存入到数据库,如果这个员工工资被扣完了,也会向系统中存入0.那么这个0就有了多重含义,而如果使用包装类类型就会避免以上情况,如果使用Double类型,忘记录入工资就会存入null,而这个员工工资被扣完了,就会存入0,不会产生歧义。
  • 持久化类要有一个唯一标识OID与表的主键对应。因为Hibernate中需要通过这个唯一标识OID区分在内存中是否是同一个持久化类。在Java中通过地址区分是否是同一个对象的,在关系型数据库的表中是通过主键区分是否同一条记录。那么Hibernate就是通过这个OID来进行区分的。Hibernate是不允许在内存中出现两个OID相同的持久化对象的。
  • 持久化类尽量不要使用final进行修饰。因为Hibernate中有延迟加载的机制,这个机制中会产生代理对象,Hibernate产生代理对象使用的是字节码的增强技术完成的,其实就是产生了当前类的一个子类对象实现的。如果使用了final修饰持久化类。那么就不能产生子类,从而就不会产生代理对象,那么Hibernate的延迟加载策略(是一种优化手段)就会失效。

简单的说:我们的持久化类(实体类)都需要遵从JavaBean的编写规范。

什么是JavaBean:
Bean:在软件开发领域,Bean表示可重用组件。
JavaBean就是用java语言开发的可重用组件。

JavaBean的编写规范是什么:
类都是public的
都有默认无参构造函数
成员变量都是私有的
都有公有的get/set方法
一般都实现Serializable接口。

基本类型和包装类的选择问题:
由于包装类可以有null值。所以实际开发中都是用包装类。

持久化类我们已经可以正常编写了,但是在持久化类中有一个特殊的属性(唯一对象标识OID),这个属性是用来与表的主键去建立映射关系。


1.2 Hibernate对象标识符(OID)

OID全称是Object Identifier,又叫做对象标识符。

它是hibernate用于区分两个对象是否是同一个对象的标识。

我们都知道:虚拟机内存区分两个对象是通过判断两个对象的内存地址是否一致;数据库区分两条记录是否相同,靠的是表的主键。hibernate负责把内存中的对象持久化到数据库表中,靠的就是对象标识符来区分两个对象是否是同一个。

在这里插入图片描述
实体类中映射主键的字段就是OID,如下图所示:
在这里插入图片描述
通常主键一般我们是不会让客户手动录入的,一般我们是由程序生成主键。那么Hibernate中也提供了相应的主键生成的方式,那么我们来看下Hibernate的主键生成策略。


1.3 Hibernate的主键生成策略

在讲解Hibernate的主键生成策略之前,先来了解两个概念,即自然主键和代理主键,具体如下:

  • 自然主键(业务主键):把具有业务含义的字段作为主键,称之为自然主键。例如在customer表中,如果把name字段作为主键,其前提条件必须是:每一个客户的姓名不允许为null,不允许客户重名,并且不允许修改客户姓名。尽管这也是可行的,但是不能满足不断变化的业务需求,一旦出现了允许客户重名的业务需求,就必须修改数据模型,重新定义表的主键,这给数据库的维护增加了难度。
  • 代理主键(逻辑主键):把不具备业务含义的字段作为主键,称之为代理主键。该字段一般取名为“ID”,通常为整数类型,因为整数类型比字符串类型要节省更多的数据库空间。在上面例子中,显然更合理的方式是使用代理主键。
    在这里插入图片描述

2.Hibernate一级缓存

2.1 什么是缓存

数据存储到数据库里面,数据库本身是一个文件系统,使用流方式操作文件(效率不高)
改进方式:把数据存到内存中,不需要使用流方式,可以直接读取内存中的数据

缓存:内存中的临时数据,当内存释放时,缓存消失。
在读取数据时,先去缓存中查找,缓存中有就直接拿出,缓存中没有则再去数据库中查询
缓存的优点:降低IO读写、提高执行效率。


2.2 一级缓存

Hibernate的一级缓存就是指Session缓存,Session缓存是一块内存空间,用来存放相互管理的java对象,在使用Hibernate查询对象的时候,首先会使用对象属性的OID值在Hibernate的一级缓存中进行查找,如果找到匹配OID值的对象,就直接将该对象从一级缓存中取出使用,不会再查询数据库;如果没有找到相同OID值的对象,则会去数据库中查找相应数据。当从数据库中查询到所需数据时,该数据信息也会放置到一级缓存中。

Hibernate的一级缓存的作用就是减少对数据库的访问次数(降低IO读写次数)。

Hibernate的一级缓存有如下特点:

  • 一级缓存默认是打开状态
  • 一级缓存的使用范围是session范围(从session创建到session关闭)
  • 一级缓存中存储的数据必须都是持久态数据

2.3 测试一级缓存
@Test
public void testCache(){
	//从Hibernate封装的工具类中获取Session对象
 	Session session=HibernateUtil.openSession();
 	//开启事务
 	Transaction tx=session.beginTransaction();
 	//第一次执行get方法:一级缓存中无数据,会去数据库中查询
	Customer c1=session.get(Customer.class, 100L);
	System.out.println("One : "+c1);
	//第二次执行get方法:一级缓存中有数据,直接获取缓存中的数据
	Customer c2=session.get(Customer.class, 100L);
	System.out.println("Two : "+c2);
	System.out.println(c1==c2);//结果为true
	tx.commit();
	session.close();
}

在这里插入图片描述

2.4 一级缓存执行过程

在这里插入图片描述

2.5 一级缓存的特性,快照机制
@Test
public void testUpdateName(){
	//从Hibernate封装的工具类中获取Session对象
 	Session session=HibernateUtil.openSession();
 	//开启事务
 	Transaction tx=session.beginTransaction();
 	//第一步:先查询出客户信息(根据ID查询)
 	Customer c=session.get(Customer.class, 95L);
     //第二步:对查询出的客户实体进行修改(修改名称)
 	c.setCustName("传智.黑马程序员");
 	//第三步:调用Hibernate方法实现更新操作
 	//session.update(c); //常规方式是要调用update方法,但此处省略看看执行结果
 	tx.commit();
	session.close();
}

在这里插入图片描述
在这里插入图片描述
解释:以上java程序中没有直接调用update方法,同样也对数据修改成功。主要是借助了Hibernate的快照功能

Hibernate 向一级缓存放入数据时,同时复制一份数据放入到Hibernate快照中,当使用commit()方法提交事务时,同时会清理Session的一级缓存,这时会使用OID判断一级缓存中的对象和快照中的对象是否一致,如果两个对象中的属性发生变化,则执行update语句,将缓存的内容同步到数据库,并更新快照;如果一致,则不执行update语句。

结论:Hibernate快照的作用就是确保一级缓存中的数据和数据库中的数据一致。


3.Hibernate对象状态

了解过Hibernate一级缓存之后,我们可以进一步来了解持久化类了。
Hibernate为了更好的来管理持久化类,特将持久化类分成了三种状态。在Hibernate中持久化的对象可以划分为三种状态,分别是瞬时态、持久态和脱管态,一个持久化类的实例可能处于三种不同状态中的某一种,三种状态的详细介绍如下。

1.瞬时态(transient)
瞬时态也称为临时态或者自由态,瞬时态的实例是由new命令创建、开辟内存空间的对象,不存在持久化标识OID(相当于主键值),尚未与Hibernate Session关联,在数据库中也没有记录,失去引用后将被JVM回收。瞬时状态的对象在内存中是孤立存在的,与数据库中的数据无任何关联,仅是一个信息携带的载体。

2.持久态(persistent)
持久态的对象存在持久化标识OID ,加入到了Session缓存中,并且相关联的Session没有关闭,在数据库中有对应的记录,每条记录只对应唯一的持久化对象,需要注意的是,持久态对象是在事务还未提交前变成持久态的。

3.脱管态(detached)
脱管态也称离线态或者游离态,当某个持久化状态的实例与Session的关联被关闭时就变成了脱管态。脱管态对象存在持久化标识OID,并且仍然与数据库中的数据存在关联,只是失去了与当前Session的关联,脱管状态对象发生改变时Hibernate不能检测到。
在这里插入图片描述

小结:
区分hibernate对象状态只有两个标识:一是否有OID、二是否和Session建立的关系

  • 临时状态:没有OID,和Session没有关系。
  • 持久化状态:有OID,和Session有关系。
  • 脱管状态:有OID,和Session没有关系。

4.Hibernate的事务控制

4.1 配置Session和线程绑定

在Hibernate中,可以通过代码来操作管理事务。例如:

Transaction tx=session.beginTransaction();//开启事务
在进行了持久化操作(save、update、delete)后,通过tx.commit()提交事务
如果事务出现异常,又通过tx.rollback()操作来撤销事务(事务回滚)

除了在代码中对事务开启,提交和回滚操作外,还可以在Hibernate的配置文件中对事务进行配置。配置文件中,可以设置事务的隔离级别。其具体的配置方法是在hibernate.cfg.xml文件中的<session-factory>标签元素中进行的。配置方法如下所示:

<!—
 事务隔离级别 
hibernate.connection.isolation = 4 
1—Read uncommitted isolation (读取未提交的内容。  最低级别,任何情况都无法保证)
2—Read committed isolation (读取提交的内容。   避免脏读的发生)
4—Repeatable read isolation (可重复读。   避免脏读和可重复读的发生)
8—Serializable isolation (可串行化。   最高级别,避免脏读、可重复读和幻读的发生)
-->
<property name="hibernate.connection.isolation">4</property>

到这我们已经设置了事务的隔离级别,那么我们在真正进行事务管理的时候,需要考虑事务的应用的场景,也就是说我们的事务控制不应该是在DAO层实现的,应该在Service层实现,并且在Service中调用多个DAO实现一个业务逻辑的操作。具体操作如下显示:
在这里插入图片描述
其实最主要的是如何保证在Service中开启的事务时使用的Session对象和DAO中多个操作使用的是同一个Session对象。

其实有两种办法可以实现:

  • 可以在业务层获取到Session,并将Session作为参数传递给DAO。
  • 可以使用ThreadLocal将业务层获取的Session绑定到当前线程中,然后在DAO中获取Session的时候,都从当前线程中获取。

其实使用第二种方式肯定是最优方案,那么具体的实现已经不用我们来完成了,Hibernate的内部已经将这个事情做完了。我们只需要完成一段配置即可。

Hibernate5中自身提供了三种管理 Session 对象的方法:

  • 1.Session 对象的生命周期与本地线程绑定
  • 2.Session 对象的生命周期与 JTA 事务绑定
  • 3.Hibernate 委托程序管理 Session 对象的生命周期

在 Hibernate 的配置文件中, hibernate.current_session_context_class 属性用于指定 Session 管理方式, 可选值包括:

  • thread:Session 对象的生命周期与本地线程绑定
  • jta:Session 对象的生命周期与 JTA 事务绑定
  • managed:Hibernate 委托程序来管理 Session 对象的生命周期
4.2 配置步骤

1.在hibernate.cfg.xml文件中配置

<!-- 把session绑定到当前线程上 -->
<property name="hibernate.current_session_context_class">thread</property>

2.获取Session时使用的方法:

/**
* 每次都是从当前线程上获取Session
* @return
*/
public static Session getCurrentSession(){
	return factory.getCurrentSession();
}		

细节:当我们把Session绑定到当前线程之后,关闭session就是hibernate来做的,我们就不用关了。

到这里我们已经对Hibernate的事务管理有了基本的了解,但是之前我们所做的CRUD的操作其实还没有查询多条记录。那如果我们需要查询多条记录要如何完成呢,我们接下来去学习一下Hibernate的其他的相关的API。


5.Hibernate查询对象的API介绍

5.1 概述

Query代表面向对象的一个Hibernate查询操作。在Hibernate中,通常使用session.createQuery()方法接受一个HQL语句,然后调用Query的list()或uniqueResult()方法执行查询。所谓的HQL是Hibernate Query Language缩写,其语法很像SQL语法,但它是完全面向对象的。

在Hibernate中使用Query对象的步骤,具体所示:

  1. 获得Hibernate的Session对象。
  2. 编写HQL语句。
  3. 调用session.createQuery 创建查询对象。
  4. 如果HQL语句包含参数,则调用Query的setXxx设置参数。
  5. 调用Query对象的方法执行查询。

HQL说明:把表的名称换成实体类名称。把表字段名称换成实体类属性名称。
例如:
SQL:select * from cst_customer where cust_name like ?
HQL:select c from Customer c where custName = ?
其中select c 可以省略,写为:from Customer where custName = ?


5.2 api

1.基本查询

@Test
public void testAllCostomer(){
	//从当前线程中获取新的Session对象
	Session  session= HibernateUtil.getCurrentSession();
	//开启事务
	Transaction tx =session.beginTransaction();
	//获取Query对象
	Query query =session.createQuery("from Customer");
	//通过Query对象的方法,获取结果集
	List list =query.list();
	//遍历集合,取出并输出对象
	for(Object obj : list){
		System.out.println(obj);
	}
	tx.commit();//提交事务。事务提交后会自动关闭Session对象
}

执行结果
在这里插入图片描述


2.条件查询

@Test
public void testCondition(){
    //从当前线程中获取新的Session对象
    Session  session= HibernateUtil.getCurrentSession();
    //开启事务
    Transaction tx =session.beginTransaction();
    //获取Query对象
    Query query =session.createQuery("from Customer where custName like ? and custLevel = ?");
    //Hibernate中的参数占位符索引是从0开始
    query.setString(0, "%集团%");//给第一个参数占位符赋值
    query.setString(1, "普通客户");//给第二个参数占位符赋值
    //调用Query对象的方法,获取结果集
    List list = query.list();
    //遍历结果集
    for(Object obj : list){
        System.out.println(obj);
    }
    tx.commit();//提交事务。 提交后会关闭Session对象
}

程序的执行结果:
在这里插入图片描述
使用参数占位符名称的代码:

/**
 * 条件查询:通过参数占位符名称给条件赋值
 * 参数占位符名称的写法:        :名称
 * 注意:在赋值时可以省略冒号,直接写"名称"即可。
 */
@Test
public void testCondition2(){
	//从当前线程中获取新的Session对象
	Session  session= HibernateUtil.getCurrentSession();
	//开启事务
	Transaction tx =session.beginTransaction();
	//获取Query对象
	Query query =session.createQuery("from Customer where custName like :custName and custLevel = :custLevel");
	//通过参数占位符名称赋值(直接写名称,不需要写冒号)
	query.setString("custLevel", "普通客户");//给第二个参数占位符赋值
	query.setString("custName", "%集团%");//给第一个参数占位符赋值
	//调用Query对象的方法,获取结果集
	List list = query.list();
	//遍历结果集
	for(Object obj : list){
		System.out.println(obj);
	}
	tx.commit();//提交事务。 提交后会关闭Session对象		
}

3.分页查询
在MySQL中使用limit进行分页: SELECT * FROM cst_customer LIMIT 0,2

Limit中参数的定义:
第一个:查询的开始记录索引
第二个:每次查询的条数

@Test
public void testPage(){
    //从当前线程中获取新的Session对象
    Session  session= HibernateUtil.getCurrentSession();
    //开启事务
    Transaction tx =session.beginTransaction();
    //获取Query对象
    Query query =session.createQuery("from Customer");
    //Hibernate中设置分页的方法
    query.setFirstResult(2);//设置记录的索引
    query.setMaxResults(2);//设置每次查询的记录条数
    //调用Query对象的方法,获取结果集
    List list = query.list();
    //遍历结果集
    for(Object obj : list){
        System.out.println(obj);
    }
    tx.commit();//提交事务。
}

程序的执行结果:
在这里插入图片描述

4.排序查询

@Test
public void testSort(){
    //从当前线程中获取新的Session对象
    Session  session= HibernateUtil.getCurrentSession();
    //开启事务
    Transaction tx =session.beginTransaction();
    //获取Query对象
    Query query =session.createQuery("from Customer order by custId desc");
    //调用Query对象的方法,获取结果集
    List list = query.list();
    //遍历结果集
    for(Object obj : list){
        System.out.println(obj);
    }
    tx.commit();//提交事务
}

在这里插入图片描述

5.统计查询

 * 带有聚合函数的HQL
 * 聚合函数:count、sum、avg、max、min
 * SQL语句在使用聚合函数时,如果没有group by分组,聚合函数返回的结果永远只有一行一列
 * 
 * count()聚合函数的几种用法:
 * select  count(*)  from  table;    会统计所有字段,执行效率比较低
 * select  count(主键)  from  table;     仅按主键字段进行统计,执行效率比较高
 * select  count(非主键)  from  table;    只统计非null字段
 * 企业开发中推荐使用:select  count(1)  from  table;    
@Test
public void TestCount(){
    //从当前线程中获取新的Session对象
    Session  session= HibernateUtil.getCurrentSession();
    //开启事务
    Transaction tx =session.beginTransaction();
    //获取Query对象
    Query query =session.createQuery("select count(custId) from Customer");
    //当确定仅有一个结果时,使用Query对象的查询唯一结果集方法
    Long count = (Long)query.uniqueResult();
    System.out.println("公司现有客户"+count+"个");
    tx.commit();//提交事务
}

程序执行结果:
在这里插入图片描述

6.投影查询

 * 投影查询
 * 投影:使用一个实体的部分字段信息,来构建实体类对象,叫做对象的投影(在hibernate中的叫法)
 * 使用HQL的方式查询实体类的部分字段信息,并且封装到实体类中。
 * HQL语句的写法:select  new Customer(custId,custName) from Customer  
 * 如果工程只有一个唯一的类,可以不写全限定类名,否则必须写全限定类名(cn.itcast.domain.类名)。
 * 实体类要求:必须提供一个相同参数列表的构造函数
//提供对应参数列表的构造函数
public class Customer implements Serializable{
	......
	public Customer(Long custId, String custName) {
		super();
		this.custId = custId;
		this.custName = custName;
	}
	......
}

@Test
public void tetsProjection(){
	//从当前线程中获取新的Session对象
	Session  session= HibernateUtil.getCurrentSession();
	//开启事务
	Transaction tx =session.beginTransaction();
	Query query=session.createQuery("select new Customer(custId,custName) from Customer");
	//调用Query对象的方法,获取结果集
	List list = query.list();
	//遍历结果集
	for(Object obj : list){
		System.out.println(obj);
	}
	tx.commit();//提交事务
}

在这里插入图片描述

5.3 Query对象中的方法说明
  • list方法:该方法用于查询语句,返回的结果是一个list集合。
  • uniqueResult方法:该方法用于查询,在确保只有一条记录的查询时可以使用该方法,返回的结果是一个Object对象
  • setter方法:Query接口中提供了一系列的setter方法用于设置查询语句中的参数,针对不同的数据类型,需要用到不同的setter方法。
  • setFirstResult()方法:该方法可以设置获取第一个记录的位置,也就是它表示从第几条记录开始查询,默认从0开始计算。
  • setMaxResult()方法:该方法用于设置结果集的最大记录数,通常与setFirstResult()方法结合使用,用于限制结果集的范围,以实现分页功能。

6.Criteria对象

6.1 概述

Criteria是一个完全面向对象,可扩展的条件查询API,通过它完全不需要考虑数据库底层如何实现,以及SQL语句如何编写,它是Hibernate框架的核心查询对象。

Criteria 查询,又称为QBC查询(Query By Criteria),它是Hibernate的另一种对象检索方式。

org.hibernate.criterion.Criterion是Hibernate提供的一个面向对象查询条件接口,一个单独的查询就是Criterion接口的一个实例,用于限制Criteria对象的查询,在Hibernate中Criterion对象的创建通常是通过Restrictions 工厂类完成的,它提供了条件查询方法。
通常,使用Criteria对象查询数据的主要步骤,具体如下:

  1. 获得Hibernate的Session对象
  2. 通过Session获得Criteria对象
  3. 使用Restrictions的静态方法创建Criterion条件对象。Restrictions类中提供了一系列用于设定查询条件的静态方法,这些静态方法都返回Criterion实例,每个Criterion实例代表一个查询条件。
  4. 向Criteria对象中添加Criterion 查询条件。Criteria的add()方法用于加入查询条件
  5. 执行Criterita的 list() 或uniqueResult() 获得结果。

细节:
HQL能查的,QBC都能查,反之亦然。
了解了Criteria对象的使用步骤后,接下来,通过具体示例来演示Criteria对象的查询操作

6.2 常用查询

1.基本查询

@Test
public void testBaseQuery(){
    //第1步:从当前线程中获取新的Session对象
    Session session= HibernateUtil.getCurrentSession();
    Transaction tx =session.beginTransaction();//开启事务
    //第2步:创建Criteria对象
    Criteria c =session.createCriteria(Customer.class);//相当于HQL的from Customer
    //执行Criteria对象的方法,获取结果集
    List list =c.list();
    //遍历
    for(Object obj : list){
        System.out.println(obj);
    }
    tx.commit();//提交事务。同时也关闭了Session
}

2.条件查询

@Test
public void testCondition(){
    //第1步:从当前线程中获取新的Session对象
    Session  session= HibernateUtil.getCurrentSession();
    Transaction tx =session.beginTransaction();//开启事务
    //第2步:创建Criteria对象
    Criteria c =session.createCriteria(Customer.class);//相当于HQL的from Customer
    //第3步:设置查询条件
    c.add(Restrictions.like("custName", "%集团%"));//相当于where custName like '%集团%'
    c.add(Restrictions.eq("custLevel", "普通客户"));//相当于and custLevel ='普通客户'
    //执行Criteria对象的方法,获取结果集
    List list =c.list();
    //遍历
    for(Object obj : list){
        System.out.println(obj);
    }
    tx.commit();//提交事务
}

3.分页查询

@Test
public void testPage(){
    //第1步:从当前线程中获取新的Session对象
    Session  session= HibernateUtil.getCurrentSession();
    Transaction tx =session.beginTransaction();//开启事务
    //第2步:创建Criteria对象
    Criteria c =session.createCriteria(Customer.class);//相当于HQL的from Customer
    //Hibernate中设置分页的方法
    c.setFirstResult(0);//设置记录的索引
    c.setMaxResults(2);//设置每次查询的记录条数
    //执行Criteria对象的方法,获取结果集
    List list = c.list();
    //遍历结果集
    for(Object obj : list){
        System.out.println(obj);
    }
    tx.commit();//提交事务。
}

4.排序查询

@Test
public void testSort(){
	//第1步:从当前线程中获取新的Session对象
	Session  session= HibernateUtil.getCurrentSession();
	Transaction tx =session.beginTransaction();//开启事务
	//第2步:创建Criteria对象
	Criteria c =session.createCriteria(Customer.class);//相当于HQL的from Customer
	//设置排序方式
	c.addOrder(Order.desc("custId"));//相当于HQL的 order by custId desc
	//执行Criteria对象的方法,获取结果集
	List list = c.list();
	//遍历结果集
	for(Object obj : list){
		System.out.println(obj);
	}
	tx.commit();//提交事务
}

5.统计查询

@Test
public void testCount(){
	//第1步:从当前线程中获取新的Session对象
	Session  session= HibernateUtil.getCurrentSession();
	Transaction tx =session.beginTransaction();//开启事务
	//第2步:创建Criteria对象
	Criteria c =session.createCriteria(Customer.class);//相当于HQL的from Customer
	// 调用
	//c.setProjection(Projections.rowCount());//相当于 select count(*)
	c.setProjection(Projections.count("custId"));//相当于 select count(cust_id) 
	//调用仅返回唯一结果的方法
	Long count = (Long)c.uniqueResult();
	System.out.println("公司现有客户"+count+"个");
	tx.commit();//提交事务
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值