Spring Boot整理——Spring Data JPA

一、基本介绍
        JPA诞生的缘由是为了整合第三方ORM框架,建立一种标准的方式,百度百科说是JDK为了实现ORM的天下归一,目前也是在按照这个方向发展,但是还没能完全实现。在ORM框架中,Hibernate是一支很大的部队,使用很广泛,也很方便,能力也很强,同时Hibernate也是和JPA整合的比较良好,我们可以认为JPA是标准,事实上也是,JPA几乎都是接口,实现都是Hibernate在做,宏观上面看,在JPA的统一之下Hibernate很良好的运行。

我们都知道,在使用持久化工具的时候,一般都有一个对象来操作数据库,在原生的Hibernate中叫做Session,在JPA中叫做EntityManager,在MyBatis中叫做SqlSession,通过这个对象来操作数据库。我们一般按照三层结构来看的话,Service层做业务逻辑处理,Dao层和数据库打交道,在Dao中,就存在着上面的对象。那么ORM框架本身提供的功能有什么呢?答案是基本的CRUD,所有的基础CRUD框架都提供,我们使用起来感觉很方便,很给力,业务逻辑层面的处理ORM是没有提供的,如果使用原生的框架,业务逻辑代码我们一般会自定义,会自己去写SQL语句,然后执行。在这个时候,Spring-data-jpa的威力就体现出来了,ORM提供的能力他都提供,ORM框架没有提供的业务逻辑功能Spring-data-jpa也提供,全方位的解决用户的需求。使用Spring-data-jpa进行开发的过程中,常用的功能,我们几乎不需要写一条sql语句。

二、环境配置
1、maven配置
        首先需要spring相关架包,其实spring-data-jpa里面已经依赖了,如果你想用自己的版本则需要额外引入spring相关包.JPA实现还都是hibernate去实现的,所以还需要hibernate相关包.mysql就更不用说了.


     
       org.springframework.data
       spring-data-jpa
       1.10.4.RELEASE
     
   

   
   
     org.hibernate
     hibernate-core
      h i b e r n a t e . v e r s i o n &lt; / v e r s i o n &gt;     &lt; / d e p e n d e n c y &gt;     &lt; d e p e n d e n c y &gt;       &lt; g r o u p I d &gt; o r g . h i b e r n a t e &lt; / g r o u p I d &gt;       &lt; a r t i f a c t I d &gt; h i b e r n a t e − e n t i t y m a n a g e r &lt; / a r t i f a c t I d &gt;       &lt; v e r s i o n &gt; {hibernate.version}&lt;/version&gt;    &lt;/dependency&gt;    &lt;dependency&gt;      &lt;groupId&gt;org.hibernate&lt;/groupId&gt;      &lt;artifactId&gt;hibernate-entitymanager&lt;/artifactId&gt;      &lt;version&gt; hibernate.version</version>  </dependency>  <dependency>   <groupId>org.hibernate</groupId>   <artifactId>hibernateentitymanager</artifactId>   <version>{hibernate.version}
   
   

   
   
     mysql
     mysql-connector-java
     ${mysql.version}
   
   
2、整合Spring
整合Spring主要有以下几点要注意:

数据源配置  
JPA提供者,JPA属性配置  
事务配置  
jpa:repositories 配置
具体如下代码:

<context:property-placeholder location=“classpath:config.properties”/>


       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
   

   
   
   
       
       
       
       
       
       
       
       
       
           
               
               org.hibernate.dialect.MySQL5Dialect
               
               false
               
               false
               
               false
               
               none
           
       
   

   
   
   
       
       
   

   
   
   
   
   
   <jpa:repositories base-package=“cn.mrdear.repository”
                     repository-impl-postfix=“Impl”
                     entity-manager-factory-ref=“entityManagerFactory”
                     transaction-manager-ref=“transactionManager”/>

   
   
   <tx:annotation-driven transaction-manager=“transactionManager”/>
3、创建实体类
实体类中常用注解:

@Entity :声明这个类是一个实体类  
 @Table:指定映射到数据库的表格  
 @Id :映射到数据库表的主键属性,一个实体只能有一个属性被映射为主键  
 @GeneratedValue:主键的生成策略  
 @Column配置单列属性 
@Entity//标识该为一个实体
@Table(name = “user”)//关联数据库中的user表
public class User {
   @Id//标识该属性为主键
   private Integer id;
   private String name;
   private String address;
   private String phone;
   //省略get和set
}
4、Repository接口
Repository: 最顶层的接口,是一个空接口,目的是为了统一所有的Repository的类型,且能让组件扫描时自动识别 
CrudRepository: Repository的子接口,提供CRUD 的功能。 
PagingAndSortingRepository:CrudRepository的子接口, 添加分页排序。 
JpaRepository: PagingAndSortingRepository的子接口,增加批量操作等。 
JpaSpecificationExecutor: 用来做复杂查询的接口。

由图来看,一般使用JpaRepository这个接口做查询即可.这个接口拥有如下方法:

delete删除或批量删除 
findOne查找单个 
findAll查找所有 
save保存单个或批量保存 
saveAndFlush保存并刷新到数据库
知道这些我们就可以创建repository 了:

//User表示该Repository与实体User关联,主键类型为Integer
public interface UserRepository extends JpaRepository<User,Integer> {
}
​这样就完成了一个基本Repository的创建,可以直接使用其中的方法,而不需要去写实现类。

5、测试
        关于测试这里,我把测试案例写到test文件夹的话,总是报实体类未被JPA管理,所以改写到java文件夹,具体原因未知.

public static void main(String[] args) {
       ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“spring/applicationContext.xml”);
       UserRepository userRepository = (UserRepository) applicationContext.getBean(“userRepository”);
       System.out.println(userRepository.findAll());
       System.out.println(userRepository.findOne(1));
       System.out.println(userRepository.findAll(new Sort(new Sort.Order(Sort.Direction.ASC,“id”))));
  }

三、CRUD操作
1、增加
        增加可以使用JpaRepository接口里面的save方法.查看源码可以发现实际上是使用了em.persist(entity)来使对象进入持久化状态,最后提交事务的时候再一起更新到数据库:

User user = new User();
user.setId(99);
user.setAddress(“上海”);
user.setName(“张三”);
user.setPhone(“110”);
//保存单个
userRepository.save(user);
//保存或更新
userRepository.saveAndFlush(user);
List users = new ArrayList<>();
users.add(user);
//保存多个
userRepository.save(users);
        这里还可以批量插入,对于mysql支持INSERT user VALUES (20,‘王二’,‘111’,‘111’),(21,‘王二’,‘111’,‘111’);类似这样的sql语句,具体实现就需要自己去写实现了,这样可以一次插入多条记录,效率很高.至于一次插入多少条就可以根据你的业务量来自己制定。

2、删除
        删除都是根据主键来删除的,区别就是多条sql和单条sql :

User user = new User();
user.setId(21);
user.setName(“王二”);
/**

  • 删除都是根据主键删除
    */
    //删除单条,根据主键值
    userRepository.delete(20);
    //删除全部,先findALL查找出来,再一条一条删除,最后提交事务
    userRepository.deleteAll();
    //删除全部,一条sql
    userRepository.deleteAllInBatch();
    List users = new ArrayList<>();
    users.add(user);
    //删除集合,一条一条删除
    userRepository.delete(users);
    //删除集合,一条sql,拼接or语句 如 id=1 or id=2
    userRepository.deleteInBatch(users);
    3、修改
            修改也是根据主键来更新的 :

User user = new User();
user.setId(1);
user.setName(“张三”);
/**

  • 更新也是根据主键来更新 update XX xx where id=1
    */
    userRepository.saveAndFlush(user);
    批量更新的话,就调用entityManager的merge函数来更新.

//首先在service层获取持久化管理器:
@PersistenceContext
private EntityManager em;
//批量更新方法,同理插入,删除也都可以如此做.
@Transactional
public void batchUpateCustom(List users) {
   // TODO Auto-generated method stub
   for(int i = 0; i < users.size(); i++) {
       em.merge(users.get(i));
       if(i % 30== 0) {
           em.flush();
           em.clear();
      }
  }
}
4、简单查询
        单表查询,大部分都可以使用下面的方法解决,多表联合查询的话,下面方法就不是很实用,下一节分析多表查询。

1.使用JpaRepository方法
//查找全部
userRepository.findAll();
//分页查询全部,返回封装了分页信息
Page pageInfo = userRepository.findAll(new PageRequest(1, 3, Sort.Direction.ASC,“id”));
//查找全部,并排序
userRepository.findAll(new Sort(new Sort.Order(Sort.Direction.ASC,“id”)));
User user = new User();
user.setName(“小红”);
//条件查询,可以联合分页,排序
userRepository.findAll(Example.of(user));
//查询单个
userRepository.findOne(1);
2.解析方法名创建查询
        规则:find+全局修饰+By+实体的属性名称+限定词+连接词+ …(其它实体属性)+OrderBy+排序属性+排序方向。例如:

//分页查询出符合姓名的记录,同理Sort也可以直接加上
public List findByName(String name, Pageable pageable);
全局修饰: Distinct, Top, First

关键词: IsNull, IsNotNull, Like, NotLike, Containing, In, NotIn,

IgnoreCase, Between, Equals, LessThan, GreaterThan, After, Before…

排序方向: Asc, Desc

连接词: And, Or

And — 等价于 SQL 中的 and 关键字,比如 findByUsernameAndPassword(String user, Striang pwd);

Or — 等价于 SQL 中的 or 关键字,比如 findByUsernameOrAddress(String user, String addr);

Between — 等价于 SQL 中的 between 关键字,比如 findBySalaryBetween(int max, int min);

LessThan — 等价于 SQL 中的 “<”,比如 findBySalaryLessThan(int max);

GreaterThan — 等价于 SQL 中的”>”,比如 findBySalaryGreaterThan(int min);

IsNull — 等价于 SQL 中的 “is null”,比如 findByUsernameIsNull();

IsNotNull — 等价于 SQL 中的 “is not null”,比如 findByUsernameIsNotNull();

NotNull — 与 IsNotNull 等价;

Like — 等价于 SQL 中的 “like”,比如 findByUsernameLike(String user);

NotLike — 等价于 SQL 中的 “not like”,比如 findByUsernameNotLike(String user);

OrderBy — 等价于 SQL 中的 “order by”,比如 findByUsernameOrderBySalaryAsc(String user);

Not — 等价于 SQL 中的 “! =”,比如 findByUsernameNot(String user);

In — 等价于 SQL 中的 “in”,比如 findByUsernameIn(Collection userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;

NotIn — 等价于 SQL 中的 “not in”,比如 findByUsernameNotIn(Collection userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;

嵌套实体:

主实体中子实体的名称+ _ +子实体的属性名称

List findByAddress_ZipCode(ZipCode zipCode)

表示查询所有 Address(地址)的zipCode(邮编)为指定值的所有Person(人员)

3.JPQL查询
        一个类似HQL的语法,在接口上使用@Query标识:

@Query(“select a from user a where a.id = ?1”)
public User findById(Long id);
使用@Modifying标识修改

@Modifying
@Query(“update User a set a.name = ?1 where a.id < ?2”)
public int updateName(String name, Long id);
携带分页信息:

@Query(“select u from User u where u.name=?1”)
public List findByName(String name, Pageable pageable);
        除此之外也可以使用原生sql,只需要@Query(nativeQuery=true)标识即可.

创建查询顺序:

Spring Data JPA 在为接口创建代理对象时,如果发现同时存在多种上述情况可用,它该优先采用哪种策略呢?为此, 提供了 query-lookup-strategy 属性,用以指定查找的顺序。它有如下三个取值:

create — 通过解析方法名字来创建查询。即使有符合的命名查询,或者方法通过 @Query 指定的查询语句,都将会被忽略。
create-if-not-found — 如果方法通过 @Query 指定了查询语句,则使用该语句实现查询;如果没有,则查找是否定义了符合条件的命名查询,如果找到,则使用该命名查询;如果两者都没有找到,则通过解析方法名字来创建查询。这是 query-lookup-strategy 属性的默认值。
use-declared-query — 如果方法通过 @Query 指定了查询语句,则使用该语句实现查询;如果没有,则查找是否定义了符合条件的命名查询,如果找到,则使用该命名查询;如果两者都没有找到,则抛出异常。
4.计数
        计数就直接使用JpaRepository的count方法:

//查找总数量
userRepository.count();
User user = new User();
user.setName(“小红”);
//条件计数
userRepository.count(Example.of(user));
5.判断是否存在
        计数就直接使用JpaRepository的exists方法:

//根据主键判断是否存在
userRepository.exists(1);
User user = new User();
user.setName(“小红”);
//根据条件判断是否存在
userRepository.exists(Example.of(user));
6.自定义查询
        首先自定义一个接口,用于定义自定义方法,如UserRepositoryCustom,然后让UserRepository实现该接口,这样的话就可以使用其中的方法。然后写UserRepositoryImpl实现UserRepositoryCustom接口。

最后设置jpa:repositories的repository-impl-postfix=“Impl”,这样的话JPA会查找自定义实现类命名规则,这样的话JPA在相应UserRepository包下面查找实现类,找到则会使用其中的实现方法,而不去自己实现。具体可以看项目demo,或者下一节的复杂查询。

5、复杂查询
1.使用CriteriaBuilder构建JPQL
        在UserRepositoryImpl中使用CriteriaBuilder实现根据id查询,下面是代码:

public void findById(Integer id){
   //select u from User u where u.id = 1
   CriteriaBuilder cb = entityManager.getCriteriaBuilder();
   CriteriaQuery cq = cb.createQuery(User.class);
   Root root = cq.from(User.class); //from User
   cq.select(root); //select * from User
   javax.persistence.criteria.Predicate pre = cb.equal(root.get(“id”).as(Integer.class),id);//id=1
   cq.where(pre);//where id=1
   Query query = entityManager.createQuery(cq);//select u from User u where u.id = 1

   System.out.println(query.getResultList());
}
缺点:

代码量多
 不易维护
 条件复杂的话,则会显得很混乱.
2.使用JpaSpecificationExecutor查询
        该接口有如下方法,里面传入条件都是Specification,该接口会返回一个Predicate条件集合,因此就可以在这里封装:

public interface JpaSpecificationExecutor {
  T findOne(Specification spec);
  List findAll(Specification spec);
  Page findAll(Specification spec, Pageable pageable);
  List findAll(Specification spec, Sort sort);
  long count(Specification spec);
}
1.构造过滤条件集合

Operator枚举类里面的operator属性为了构建原生sql使用:

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

import java.io.Serializable;

/**

  • 筛选
    /
    public class Filter implements Serializable {

       private static final long serialVersionUID = -8712382358441065075L;

       /
    *
        * 运算符
        /
       public enum Operator {

           /
    * 等于 /
           eq(" = "),

           /
    * 不等于 /
           ne(" != "),

           /
    * 大于 /
           gt(" > "),

           /
    * 小于 /
           lt(" < "),

           /
    * 大于等于 /
           ge(" >= "),

           /
    * 小于等于 /
           le(" <= "),

           /
    * 类似 /
           like(" like "),

           /
    * 包含 /
           in(" in "),

           /
    * 为Null /
           isNull(" is NULL "),

           /
    * 不为Null /
           isNotNull(" is not NULL ");
           Operator(String operator) {
               this.operator = operator;
          }

           private String operator;

           public String getOperator() {
               return operator;
          }

           public void setOperator(String operator) {
               this.operator = operator;
          }
      }

       /
    * 默认是否忽略大小写 /
       private static final boolean DEFAULT_IGNORE_CASE = false;

       /
    * 属性 /
       private String property;

       /
    * 运算符 /
       private Filter.Operator operator;

       /
    * 值 /
       private Object value;

       /
    * 是否忽略大小写 /
       private Boolean ignoreCase = DEFAULT_IGNORE_CASE;

       /
    *
        * 构造方法
        /
       public Filter() {
      }

       /
    *
        * 构造方法
        *
        * @param property
        *           属性
        * @param operator
        *           运算符
        * @param value
        *           值
        /
       public Filter(String property, Filter.Operator operator, Object value) {
           this.property = property;
           this.operator = operator;
           this.value = value;
      }

       /
    *
        * 构造方法
        *
        * @param property
        *           属性
        * @param operator
        *           运算符
        * @param value
        *           值
        * @param ignoreCase
        *           忽略大小写
        /
       public Filter(String property, Filter.Operator operator, Object value, boolean ignoreCase) {
           this.property = property;
           this.operator = operator;
           this.value = value;
           this.ignoreCase = ignoreCase;
      }

       /
    *
        * 返回等于筛选
        *
        * @param property
        *           属性
        * @param value
        *           值
        * @return 等于筛选
        /
       public static Filter eq(String property, Object value) {
           return new Filter(property, Filter.Operator.eq, value);
      }

       /
    *
        * 返回等于筛选
        *
        * @param property
        *           属性
        * @param value
        *           值
        * @param ignoreCase
        *           忽略大小写
        * @return 等于筛选
        /
       public static Filter eq(String property, Object value, boolean ignoreCase) {
           return new Filter(property, Filter.Operator.eq, value, ignoreCase);
      }

       /
    *
        * 返回不等于筛选
        *
        * @param property
        *           属性
        * @param value
        *           值
        * @return 不等于筛选
        /
       public static Filter ne(String property, Object value) {
           return new Filter(property, Filter.Operator.ne, value);
      }

       /
    *
        * 返回不等于筛选
        *
        * @param property
        *           属性
        * @param value
        *           值
        * @param ignoreCase
        *           忽略大小写
        * @return 不等于筛选
        /
       public static Filter ne(String property, Object value, boolean ignoreCase) {
           return new Filter(property, Filter.Operator.ne, value, ignoreCase);
      }

       /
    *
        * 返回大于筛选
        *
        * @param property
        *           属性
        * @param value
        *           值
        * @return 大于筛选
        /
       public static Filter gt(String property, Object value) {
           return new Filter(property, Filter.Operator.gt, value);
      }

       /
    *
        * 返回小于筛选
        *
        * @param property
        *           属性
        * @param value
        *           值
        * @return 小于筛选
        /
       public static Filter lt(String property, Object value) {
           return new Filter(property, Filter.Operator.lt, value);
      }

       /
    *
        * 返回大于等于筛选
        *
        * @param property
        *           属性
        * @param value
        *           值
        * @return 大于等于筛选
        /
       public static Filter ge(String property, Object value) {
           return new Filter(property, Filter.Operator.ge, value);
      }

       /
    *
        * 返回小于等于筛选
        *
        * @param property
        *           属性
        * @param value
        *           值
        * @return 小于等于筛选
        /
       public static Filter le(String property, Object value) {
           return new Filter(property, Filter.Operator.le, value);
      }

       /
    *
        * 返回相似筛选
        *
        * @param property
        *           属性
        * @param value
        *           值
        * @return 相似筛选
        /
       public static Filter like(String property, Object value) {
           return new Filter(property, Filter.Operator.like, value);
      }

       /
    *
        * 返回包含筛选
        *
        * @param property
        *           属性
        * @param value
        *           值
        * @return 包含筛选
        /
       public static Filter in(String property, Object value) {
           return new Filter(property, Filter.Operator.in, value);
      }

       /
    *
        * 返回为Null筛选
        *
        * @param property
        *           属性
        * @return 为Null筛选
        /
       public static Filter isNull(String property) {
           return new Filter(property, Filter.Operator.isNull, null);
      }

       /
    *
        * 返回不为Null筛选
        *
        * @param property
        *           属性
        * @return 不为Null筛选
        /
       public static Filter isNotNull(String property) {
           return new Filter(property, Filter.Operator.isNotNull, null);
      }

       /
    *
        * 返回忽略大小写筛选
        *
        * @return 忽略大小写筛选
        /
       public Filter ignoreCase() {
           this.ignoreCase = true;
           return this;
      }

       /
    *
        * 获取属性
        *
        * @return 属性
        /
       public String getProperty() {
           return property;
      }

       /
    *
        * 设置属性
        *
        * @param property
        *           属性
        /
       public void setProperty(String property) {
           this.property = property;
      }

       /
    *
        * 获取运算符
        *
        * @return 运算符
        /
       public Filter.Operator getOperator() {
           return operator;
      }

       /
    *
        * 设置运算符
        *
        * @param operator
        *           运算符
        /
       public void setOperator(Filter.Operator operator) {
           this.operator = operator;
      }

       /
    *
        * 获取值
        *
        * @return 值
        /
       public Object getValue() {
           return value;
      }

       /
    *
        * 设置值
        *
        * @param value
        *           值
        /
       public void setValue(Object value) {
           this.value = value;
      }

       /
    *
        * 获取是否忽略大小写
        *
        * @return 是否忽略大小写
        /
       public Boolean getIgnoreCase() {
           return ignoreCase;
      }

       /
    *
        * 设置是否忽略大小写
        *
        * @param ignoreCase
        *           是否忽略大小写
        /
       public void setIgnoreCase(Boolean ignoreCase) {
           this.ignoreCase = ignoreCase;
      }

       /
    *
        * 重写equals方法
        *
        * @param obj
        *           对象
        * @return 是否相等
        /
       @Override
       public boolean equals(Object obj) {
           if (obj == null) {
               return false;
          }
           if (getClass() != obj.getClass()) {
               return false;
          }
           if (this == obj) {
               return true;
          }
           Filter other = (Filter) obj;
           return new EqualsBuilder().append(getProperty(), other.getProperty()).append(getOperator(), other.getOperator()).append(getValue(), other.getValue()).isEquals();
      }

       /
    *
        * 重写hashCode方法
        *
        * @return HashCode
        */
       @Override
       public int hashCode() {
           return new HashCodeBuilder(17, 37).append(getProperty()).append(getOperator()).append(getValue()).toHashCode();
      }
    }
    2.构造排序字段

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import java.io.Serializable;
/**

  • 排序
  • @author copy from shopxx
    /
    public class Order implements Serializable {

       private static final long serialVersionUID = -3078342809727773232L;

       /
    *
        * 方向
        /
       public enum Direction {

           /
    * 递增 /
           asc,

           /
    * 递减 /
           desc
      }

       /
    * 默认方向 /
       private static final Order.Direction DEFAULT_DIRECTION = Order.Direction.desc;

       /
    * 属性 /
       private String property;

       /
    * 方向 /
       private Order.Direction direction = DEFAULT_DIRECTION;

       @Override
       public String toString() {
           return property+" " + direction.name();
      }

       /
    *
        * 构造方法
        /
       public Order() {
      }

       /
    *
        * 构造方法
        *
        * @param property
        *           属性
        * @param direction
        *           方向
        /
       public Order(String property, Order.Direction direction) {
           this.property = property;
           this.direction = direction;
      }

       /
    *
        * 返回递增排序
        *
        * @param property
        *           属性
        * @return 递增排序
        /
       public static Order asc(String property) {
           return new Order(property, Order.Direction.asc);
      }

       /
    *
        * 返回递减排序
        *
        * @param property
        *           属性
        * @return 递减排序
        /
       public static Order desc(String property) {
           return new Order(property, Order.Direction.desc);
      }

       /
    *
        * 获取属性
        *
        * @return 属性
        /
       public String getProperty() {
           return property;
      }

       /
    *
        * 设置属性
        *
        * @param property
        *           属性
        /
       public void setProperty(String property) {
           this.property = property;
      }

       /
    *
        * 获取方向
        *
        * @return 方向
        /
       public Order.Direction getDirection() {
           return direction;
      }

       /
    *
        * 设置方向
        *
        * @param direction
        *           方向
        /
       public void setDirection(Order.Direction direction) {
           this.direction = direction;
      }

       /
    *
        * 重写equals方法
        *
        * @param obj
        *           对象
        * @return 是否相等
        /
       @Override
       public boolean equals(Object obj) {
           if (obj == null) {
               return false;
          }
           if (getClass() != obj.getClass()) {
               return false;
          }
           if (this == obj) {
               return true;
          }
           Order other = (Order) obj;
           return new EqualsBuilder().append(getProperty(), other.getProperty()).append(getDirection(), other.getDirection()).isEquals();
      }

       /
    *
        * 重写hashCode方法
        *
        * @return HashCode
        */
       @Override
       public int hashCode() {
           return new HashCodeBuilder(17, 37).append(getProperty()).append(getDirection()).toHashCode();
      }
    }
    3.查询语句生成

1.基本框架

/**

  • 封装查询条件的实体
    /
    public class QueryParams implements Specification {

       /
    * 属性分隔符 /
       private static final String PROPERTY_SEPARATOR = “.”;
       /
    *
        * and条件
        /
       private List andFilters = new ArrayList<>();
       /
    *
        * or条件
        /
       private List orFilters = new ArrayList<>();
       /
    *
        * 排序属性
        /
       private List orders = new ArrayList<>();
           /
    *
        * 获取Path
        *
        * @param path
        *           Path
        * @param propertyPath
        *           属性路径
        * @return Path
        */
       @SuppressWarnings(“unchecked”)
       private Path getPath(Path<?> path, String propertyPath) {
           if (path == null || StringUtils.isEmpty(propertyPath)) {
               return (Path) path;
          }
           String property = StringUtils.substringBefore(propertyPath, PROPERTY_SEPARATOR);
           return getPath(path.get(property), StringUtils.substringAfter(propertyPath, PROPERTY_SEPARATOR));
      }
    }
    2.分析and条件

/**

  • 转换为Predicate
    */
    @SuppressWarnings(“unchecked”)
    private Predicate toAndPredicate(Root root,CriteriaBuilder criteriaBuilder) {
       Predicate restrictions = criteriaBuilder.conjunction();
       if (root == null || CollectionUtils.isEmpty(andFilters)) {
           return restrictions;
      }
       for (Filter filter : andFilters) {
           if (filter == null) {
               continue;
          }
           String property = filter.getProperty();
           Filter.Operator operator = filter.getOperator();
           Object value = filter.getValue();
           Boolean ignoreCase = filter.getIgnoreCase();
           Path<?> path = getPath(root, property);
           if (path == null) {
               continue;
          }
           //根据运算符生成相应条件
           switch (operator) {
               case eq:
                   if (value != null) {
                       if (BooleanUtils.isTrue(ignoreCase) && String.class.isAssignableFrom(path.getJavaType()) && value instanceof String) {
                           restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.equal(criteriaBuilder.lower((Path) path), ((String) value).toLowerCase()));
                      } else {
                           restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.equal(path, value));
                      }
                  } else {
                       restrictions = criteriaBuilder.and(restrictions, path.isNull());
                  }
                   break;
               case ne:
                   if (value != null) {
                       if (BooleanUtils.isTrue(ignoreCase) && String.class.isAssignableFrom(path.getJavaType()) && value instanceof String) {
                           restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.notEqual(criteriaBuilder.lower((Path) path), ((String) value).toLowerCase()));
                      } else {
                           restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.notEqual(path, value));
                      }
                  } else {
                       restrictions = criteriaBuilder.and(restrictions, path.isNotNull());
                  }
                   break;
               case gt:
                   if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
                       restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.gt((Path) path, (Number) value));
                  }
                   break;
               case lt:
                   if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
                       restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.lt((Path) path, (Number) value));
                  }
                   break;
               case ge:
                   if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
                       restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.ge((Path) path, (Number) value));
                  }
                   break;
               case le:
                   if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
                       restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.le((Path) path, (Number) value));
                  }
                   break;
               case like:
                   if (String.class.isAssignableFrom(path.getJavaType()) && value instanceof String) {
                       if (BooleanUtils.isTrue(ignoreCase)) {
                           restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.like(criteriaBuilder.lower((Path) path), ((String) value).toLowerCase()));
                      } else {
                           restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.like((Path) path, (String) value));
                      }
                  }
                   break;
               case in:
                   restrictions = criteriaBuilder.and(restrictions, path.in(value));
                   break;
               case isNull:
                   restrictions = criteriaBuilder.and(restrictions, path.isNull());
                   break;
               case isNotNull:
                   restrictions = criteriaBuilder.and(restrictions, path.isNotNull());
                   break;
          }
      }
       return restrictions;
    }
    3.分析or条件

把and中的and改为or即可:

/**

  • 转换为Predicate
    */
    @SuppressWarnings(“unchecked”)
    private Predicate toOrPredicate(Root root,CriteriaBuilder criteriaBuilder) {
       Predicate restrictions = criteriaBuilder.disjunction();
       if (root == null || CollectionUtils.isEmpty(andFilters)) {
           return restrictions;
      }
       for (Filter filter : orFilters) {
           if (filter == null) {
               continue;
          }
           String property = filter.getProperty();
           Filter.Operator operator = filter.getOperator();
           Object value = filter.getValue();
           Boolean ignoreCase = filter.getIgnoreCase();
           Path<?> path = getPath(root, property);
           if (path == null) {
               continue;
          }
           switch (operator) {
               case eq:
                   if (value != null) {
                       if (BooleanUtils.isTrue(ignoreCase) && String.class.isAssignableFrom(path.getJavaType()) && value instanceof String) {
                           restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.equal(criteriaBuilder.lower((Path) path), ((String) value).toLowerCase()));
                      } else {
                           restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.equal(path, value));
                      }
                  } else {
                       restrictions = criteriaBuilder.or(restrictions, path.isNull());
                  }
                   break;
               case ne:
                   if (value != null) {
                       if (BooleanUtils.isTrue(ignoreCase) && String.class.isAssignableFrom(path.getJavaType()) && value instanceof String) {
                           restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.notEqual(criteriaBuilder.lower((Path) path), ((String) value).toLowerCase()));
                      } else {
                           restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.notEqual(path, value));
                      }
                  } else {
                       restrictions = criteriaBuilder.or(restrictions, path.isNotNull());
                  }
                   break;
               case gt:
                   if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
                       restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.gt((Path) path, (Number) value));
                  }
                   break;
               case lt:
                   if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
                       restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.lt((Path) path, (Number) value));
                  }
                   break;
               case ge:
                   if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
                       restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.ge((Path) path, (Number) value));
                  }
                   break;
               case le:
                   if (Number.class.isAssignableFrom(path.getJavaType()) && value instanceof Number) {
                       restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.le((Path) path, (Number) value));
                  }
                   break;
               case like:
                   if (String.class.isAssignableFrom(path.getJavaType()) && value instanceof String) {
                       if (BooleanUtils.isTrue(ignoreCase)) {
                           restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.like(criteriaBuilder.lower((Path) path), ((String) value).toLowerCase()));
                      } else {
                           restrictions = criteriaBuilder.or(restrictions, criteriaBuilder.like((Path) path, (String) value));
                      }
                  }
                   break;
               case in:
                   restrictions = criteriaBuilder.or(restrictions, path.in(value));
                   break;
               case isNull:
                   restrictions = criteriaBuilder.or(restrictions, path.isNull());
                   break;
               case isNotNull:
                   restrictions = criteriaBuilder.or(restrictions, path.isNotNull());
                   break;
          }
      }
       return restrictions;
    }
    4.分析排序条件

/**

  • 转换为Order
    */
    private List<javax.persistence.criteria.Order> toOrders(Root root,CriteriaBuilder criteriaBuilder) {
       List<javax.persistence.criteria.Order> orderList = new ArrayList<javax.persistence.criteria.Order>();
       if (root == null || CollectionUtils.isEmpty(orders)) {
           return orderList;
      }
       for (Order order : orders) {
           if (order == null) {
               continue;
          }
           String property = order.getProperty();
           Order.Direction direction = order.getDirection();
           Path<?> path = getPath(root, property);
           if (path == null || direction == null) {
               continue;
          }
           switch (direction) {
               case asc:
                   orderList.add(criteriaBuilder.asc(path));
                   break;
               case desc:
                   orderList.add(criteriaBuilder.desc(path));
                   break;
          }
      }
       return orderList;
    }
    最后在toPredicate方法中构造最终条件:

/**

  • 生成条件的
  • @param root 该对象的封装
  • @param query 查询构建器
  • @param cb 构建器
  • @return 条件集合
    */
    @Override
    public Predicate toPredicate(Root root, CriteriaQuery<?> query, CriteriaBuilder cb) {
       Predicate restrictions = cb.and(toAndPredicate(root,cb));
       restrictions = cb.and(restrictions,toOrPredicate(root,cb));
       query.orderBy(toOrders(root,cb));
       return restrictions;
    }
    加上方便的链式调用方法:

/**

  • 添加一个and条件
  • @param filter 该条件
  • @return 链式调用
    /
    public  QueryParams and(Filter filter){
       this.andFilters.add(filter);
       return this;
    }
    /
    *
  • 添加多个and条件
  • @param filter 该条件
  • @return 链式调用
    /
    public  QueryParams and(Filter …filter){
       this.andFilters.addAll(Arrays.asList(filter));
       return this;
    }
    /
    *
  • 添加一个or条件
  • @param filter 该条件
  • @return 链式调用
    /
    public  QueryParams or(Filter filter){
       this.orFilters.add(filter);
       return this;
    }
    /
    *
  • 添加多个or条件
  • @param filter 该条件
  • @return 链式调用
    /
    public  QueryParams or(Filter …filter){
       this.orFilters.addAll(Arrays.asList(filter));
       return this;
    }
    /
    *
  • 升序字段
  • @param property 该字段对应变量名
  • @return 链式调用
    /
    public  QueryParams orderASC(String property){
       this.orders.add(Order.asc(property));
       return this;
    }
    /
    *
  • 降序字段
  • @param property 该字段对应变量名
  • @return 链式调用
    /
    public  QueryParams orderDESC(String property){
       this.orders.add(Order.desc(property));
       return this;
    }

    /
    *
  • 清除所有条件
  • @return 该实例
    /
    public QueryParams clearAll(){
       if (!this.andFilters.isEmpty()) this.andFilters.clear();
       if (!this.orFilters.isEmpty()) this.orFilters.clear();
       if (!this.orders.isEmpty()) this.orders.clear();
       return this;
    }
    /
    *
  • 清除and条件
  • @return 该实例
    /
    public QueryParams clearAnd(){
       if (!this.andFilters.isEmpty()) this.andFilters.clear();
       return this;
    }
    /
    *
  • 清除or条件
  • @return 该实例
    /
    public QueryParams clearOr(){
       if (!this.orFilters.isEmpty()) this.andFilters.clear();
       return this;
    }
    /
    *
  • 清除order条件
  • @return 该实例
    */
    public QueryParams clearOrder(){
       if (!this.orders.isEmpty()) this.orders.clear();
       return this;
    }
    //省略get和set方法
    4.测试

首先让PcardOrderRepository接口继承加上JpaSpecificationExecutor:

public interface PcardOrderRepository extends JpaRepository<PcardOrder,String>,PcardOrderRepositoryCustom,JpaSpecificationExecutor {
}
编写测试代码,这个使用的是CriteriaBuilder构建查询的,所以查询字段都是JPQL字段,并不是原生sql:

QueryParams queryParams = new QueryParams<>();
//使用Specification条件查询,使用JPQL字段查询
queryParams
      .and(Filter.eq(“acctId”,“0014779934917371041”),Filter.ne(“orderAmt”,0L),
               Filter.eq(“orderRespCd”,“00”))
      .or(Filter.eq(“orderTypeId”,“A003”),Filter.eq(“orderTypeId”,“A007”),
               Filter.eq(“orderTypeId”,“A021”),Filter.eq(“orderTypeId”,“A018”))
      .orderDESC(“createTime”);

Page JPQLlist = pcardOrderRepository.findAll(queryParams,new PageRequest(0,2));

//构造出来的条件
where
1=1
and pcardorder0_.acct_id=?
and pcardorder0_.order_amt<>0
and pcardorder0_.order_resp_cd=?
and (
   0=1
   or pcardorder0_.order_type_id=?
   or pcardorder0_.order_type_id=?
   or pcardorder0_.order_type_id=?
   or pcardorder0_.order_type_id=?
)
order by
pcardorder0_.create_time desc limit ?
3.原生sql查询
        还是利用上面的Filter,具体还是遍历+拼接,示例中我卸载了公共方法中,需要使用的Impl直接extends即可。

1.解析条件

解析条件实际上就是拼接sql,代码很简单:

/**

  • 公共方法的repository
    /
    @NoRepositoryBean
    public class BaseRepository {

       private static Logger logger = LoggerFactory.getLogger(BaseRepository.class);

       /
    *
        * 分析查询参数,并且合并到sql语句中
        * @param sql JPQL查询语句
        * @param params 查询参数
        * @return 参数对应的value
        */
       @SuppressWarnings(“Unchecked”)
       protected List analysisQueryParams(StringBuilder sql, QueryParams<?> params){
           List strList = new ArrayList<>();
           List valueList = new ArrayList<>();
           int i = 1;
           //分析or条件
           for (Filter filter : params.getOrFilters()) {
               if (filter.getValue() != null){
                   strList.add(filter.getProperty()+" " + filter.getOperator().getOperator()+" ?" + (i++));
                   valueList.add(filter.getValue());
              }else {
                   strList.add(filter.getProperty()+" " + filter.getOperator().getOperator()+" “);
              }
          }
           if (!strList.isEmpty()){
               sql.append(” and “).append(”( “).append(StringUtils.join(strList,” or “)).append(” )");
          }
           strList.clear();
           //分析and条件
           for (Filter filter : params.getAndFilters()) {
               if (filter.getValue() != null){
                   strList.add(filter.getProperty()+" " + filter.getOperator().getOperator()+" ?" + (i++));
                   valueList.add(filter.getValue());
              }else {
                   strList.add(filter.getProperty()+" " + filter.getOperator().getOperator()+" “);
              }
          }
           sql.append(” and “).append(StringUtils.join(strList,” and “));
           //分析排序字段
           if (!params.getOrders().isEmpty()){
               sql.append(” order by “);
               sql.append(StringUtils.join(params.getOrders(),”,"));
          }
           logger.debug(“解析后的sql:”+sql.toString());
           logger.debug(“对应的值为:”+valueList);
           return valueList;
      }

    }
    2.测试

在自定义接口中加入方法:

public interface PcardOrderRepositoryCustom {
   List findByQueryParam(QueryParams queryParams, Pageable pageable);
}
然后实现类中需要写部分sql

@NoRepositoryBean
public class PcardOrderRepositoryImpl extends BaseRepository implements PcardOrderRepositoryCustom {

   @PersistenceContext
   private EntityManager entityManager;
   @Override
   public List findByQueryParam(QueryParams queryParams, Pageable pageable) {
       StringBuilder sql = new StringBuilder("select * from tbl_pcard_order where 1=1 ");
       List values = analysisQueryParams(sql,queryParams);
       Query query = entityManager.createNativeQuery(sql.toString());
       for (int i = 0; i < values.size(); i++) {
           query.setParameter(i+1,values.get(i));
      }
       query.setFirstResult(pageable.getOffset());
       query.setMaxResults(pageable.getPageSize());
       return query.getResultList();
  }
}
测试代码:

//使用原生sql查询,注意这里使用原生sql字段,并非JPQL字段,
//本质是根据BaseRepository.analysisQueryParams 来拼接条件,可以根据自己的需求更改
queryParams.clearAll()
      .and(Filter.eq(“acct_id”,“0014779934917371041”),Filter.ne(“order_amt”,0L),
               Filter.eq(“order_resp_cd”,“00”))
      .or(Filter.eq(“order_type_id”,“A003”),Filter.eq(“order_type_id”,“A007”),
               Filter.eq(“order_type_id”,“A021”),Filter.eq(“order_type_id”,“A018”))
      .orderDESC(“create_time”);
List nativeSqlList = pcardOrderRepository.findByQueryParam(queryParams,new PageRequest(0,2));
//构造出来的sql
where
1=1
and (
   order_type_id  =  ?
   or order_type_id  =  ?
   or order_type_id  =  ?
   or order_type_id  =  ?
)
and acct_id  =  ?
and order_amt  !=  ?
and order_resp_cd  =  ?
order by
create_time desc limit ?
4.使用原生sql查询出Map集合
        使用原生sql进行表关联查询,返回值一般都用List:

public List<Object[]> findById(int id) {
   String sql = “select u.,c. from user u,commont c where u.id = c.id and id=?1”;
   Query query = entityManager.createNativeQuery(sql);
   query.setParameter(1,id);
   return query.getResultList();
}
那么就要改进,使其返回Map:

public List findById (int id) {
   String sql = “select u.,c. from user u,commont c where u.id = c.id and id=?1”;
   Query query = entityManager.createNativeQuery(sql);
   query.setParameter(1,id);
   //转换为Map集合
   query.unwrap(org.hibernate.SQLQuery.class).setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
   return query.getResultList();
}

//实际上返回的是一个List集合
这样的返回值是一个List类型,取出的时候直接根据键值取即可。

四、项目实战
1、基本配置
        参考《 Spring Boot整理——Thymeleaf模板》的项目,首先修改build.gradle的配置如下:

// buildscript 代码块中脚本优先执行
buildscript {

// ext 用于定义动态属性
ext {
springBootVersion = ‘1.5.2.RELEASE’
}

// 自定义  Thymeleaf 和 Thymeleaf Layout Dialect 的版本
ext['thymeleaf.version'] = '3.0.3.RELEASE'
ext['thymeleaf-layout-dialect.version'] = '2.2.0'

// 自定义  Hibernate 的版本
ext['hibernate.version'] = '5.2.8.Final'

// 使用了 Maven 的中央仓库(你也可以指定其他仓库)
repositories {
    //mavenCentral()
    maven {
        url 'http://maven.aliyun.com/nexus/content/groups/public/'
    }
}

// 依赖关系
dependencies {
    // classpath 声明说明了在执行其余的脚本时,ClassLoader 可以使用这些依赖项
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}

}

// 使用插件
apply plugin: ‘java’
apply plugin: ‘eclipse’
apply plugin: ‘org.springframework.boot’

// 打包的类型为 jar,并指定了生成的打包的文件名称和版本
jar {
baseName = ‘jpa-in-action’
version = ‘1.0.0’
}

// 指定编译 .java 文件的 JDK 版本
sourceCompatibility = 1.8

// 默认使用了 Maven 的中央仓库。这里改用自定义的镜像库
repositories {
//mavenCentral()
maven {
url ‘http://maven.aliyun.com/nexus/content/groups/public/
}
}

// 依赖关系
dependencies {
// 该依赖对于编译发行是必须的
compile(‘org.springframework.boot:spring-boot-starter-web’)

// 添加 Thymeleaf 的依赖
compile('org.springframework.boot:spring-boot-starter-thymeleaf')


// 添加 Spring Data JPA 的依赖
compile(‘org.springframework.boot:spring-boot-starter-data-jpa’)

// 添加 MySQL连接驱动 的依赖
compile('mysql:mysql-connector-java:6.0.5')


// 该依赖对于编译测试是必须的,默认包含编译产品依赖和编译时依
testCompile(‘org.springframework.boot:spring-boot-starter-test’)

//添加H2的依赖
runtime('com.h2database:h2:1.4.193')

}
然后application.properties为:

THYMELEAF

spring.thymeleaf.encoding=UTF-8

热部署静态文件

spring.thymeleaf.cache=false

使用HTML5标准

spring.thymeleaf.mode=HTML5

#使用H2 控制台   访问页面http://localhost:8080/h2-console/,
spring.h2.console.enabled=true

DataSource 测试时可以先不用mysql数据库

#spring.datasource.url=jdbc:mysql://localhost/blog?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
#spring.datasource.username=root
#spring.datasource.password=123456
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

JPA

spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto=create-drop
注意:在测试阶段可以使用H2数据库,其管理页面如下(http://localhost:8080/h2-console/  ),默认的数据库为jdbc:h2:mem:testdb,如果启用mysql,把mysql注释放开即可。

2、实体改造
@Entity  // 实体
public class User implements Serializable{

private static final long serialVersionUID = 1L;

@Id  // 主键

@GeneratedValue(strategy=GenerationType.IDENTITY) // 自增长策略
private Long id; // 用户的唯一标识

@Column(nullable = false) // 映射为字段,值不能为空
private String name;

@Column(nullable = false)
private Integer age;


@Override
   public String toString() {
       return String.format(
               “User[id=%d, name=’%s’, age=’%d’]”,
               id, name, age);
  }
}
3、持久化层改造
接口改成如下内容,实现类删除,用spring 提供的

/**

  • 用户仓库.
    */
    public interface UserRepository extends CrudRepository<User, Long>{
    }
    4、控制层改造
    @RestController
    @RequestMapping("/users")
    public class UserController {

    @Autowired
    private UserRepository userRepository;

    /**

    • 从 用户存储库 获取用户列表
    • @return
      /
      private List getUserlist() {
      List users = new ArrayList<>();
      for (User user : userRepository.findAll()) {
      users.add(user);
      }
      return users;
      }

      /
      *
    • 查询所用用户
    • @return
      */
      @GetMapping
      public ModelAndView list(Model model) {
      model.addAttribute(“userList”, getUserlist());
      model.addAttribute(“title”, “用户管理”);
      return new ModelAndView(“users/list”, “userModel”, model);
      }

    /**

    • 根据id查询用户
    • @param message
    • @return
      /
      @GetMapping("{id}")
      public ModelAndView view(@PathVariable(“id”) Long id, Model model) {
      User user = userRepository.findOne(id);
      model.addAttribute(“user”, user);
      model.addAttribute(“title”, “查看用户”);
      return new ModelAndView(“users/view”, “userModel”, model);
      }

      /
      *
    • 获取 form 表单页面
    • @param user
    • @return
      /
      @GetMapping("/form")
      public ModelAndView createForm(Model model) {
      model.addAttribute(“user”, new User(null, null));
      model.addAttribute(“title”, “创建用户”);
      return new ModelAndView(“users/form”, “userModel”, model);
      }

      /
      *
    • 新建用户
    • @param user
    • @param result
    • @param redirect
    • @return
      /
      @PostMapping
      public ModelAndView create(User user) {
      userRepository.save(user);
      return new ModelAndView(“redirect:/users”);
      }

      /
      *
    • 删除用户
    • @param id
    • @return
      /
      @GetMapping(value = “delete/{id}”)
      public ModelAndView delete(@PathVariable(“id”) Long id, Model model) {
      userRepository.delete(id);
      model.addAttribute(“userList”, getUserlist());
      model.addAttribute(“title”, “删除用户”);
      return new ModelAndView(“users/list”, “userModel”, model);
      }

      /
      *
    • 修改用户
    • @param user
    • @return
      */
      @GetMapping(value = “modify/{id}”)
      public ModelAndView modifyForm(@PathVariable(“id”) Long id, Model model) {
      User user = userRepository.findOne(id);
      model.addAttribute(“user”, user);
      model.addAttribute(“title”, “修改用户”);
      return new ModelAndView(“users/form”, “userModel”, model);
      }

      }

作者:局外人F
来源:CSDN
原文:https://blog.csdn.net/qq_22172133/article/details/81192040
版权声明:本文为博主原创文章,转载请附上博文链接!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值