Spring Data JPA动态SQL及自定义Repository

本文详细介绍了SpringDataJPA中的QueryByExampleExecutor和JpaSpecificationExecutor的使用,包括它们的原理、应用场景以及如何自定义Repository。QueryByExampleExecutor适合简单的动态查询,而JpaSpecificationExecutor则通过CriteriaAPI支持更复杂的查询需求。文章还探讨了如何通过SpecificationFactory和自定义Repository实现更优雅的查询逻辑,并给出了实际工作中的应用案例。
摘要由CSDN通过智能技术生成

Spring Data JPA动态SQL及自定义Repository

从 JpaRepository 开始的子类,都是 Spring Data 项目对 JPA 实现的封装与扩展。JpaRepository 本身继承 PagingAndSortingRepository 接口,是针对 JPA 技术的接口,提供 flush()、saveAndFlush()、deleteInBatch()、deleteAllInBatch() 等方法。我们来看一下 UML 来对 JpaRespository 有个整体的认识。

011805168bb9cad6f0e7288d609cc653.png
QueryByExampleExecutor的使用

按实例查询(QBE)是一种用户友好的查询技术,具有简单的接口。它允许动态查询创建,并且不需要编写包含字段名称的查询。只需要继承JpaRepository接口后,自动拥有了实例查询方法。

public interface QueryByExampleExecutor<T> {
        // 根据实例创建一个对象
    <S extends T> Optional<S> findOne(Example<S> var1);
        // 根据实例查找一批对象        
    <S extends T> Iterable<S> findAll(Example<S> var1);
        // 根据实例查找一批对象,且排序
    <S extends T> Iterable<S> findAll(Example<S> var1, Sort var2);
        // 根据实例查找一批对象,且排序和分页
    <S extends T> Page<S> findAll(Example<S> var1, Pageable var2);
        // 根据实例查找,返回符合条件的个数
    <S extends T> long count(Example<S> var1);
        // 根据实例查找,判断是否有符合条件的对象
    <S extends T> boolean exists(Example<S> var1);
}

需要掌握操作Example的用法和API

public interface Example<T> {
    static <T> Example<T> of(T probe) {
        return new TypedExample(probe, ExampleMatcher.matching());
    }
 
    static <T> Example<T> of(T probe, ExampleMatcher matcher) {
        return new TypedExample(probe, matcher);
    }
 
    T getProbe();
 
    ExampleMatcher getMatcher();
 
    default Class<T> getProbeType() {
        return ProxyUtils.getUserClass(this.getProbe().getClass());
    }
}


在源码中Example主要包括三个内容:

  • Probe:这是具有填充字段的域对象实际实体类,即查询条件的封装类。必填
  • ExampleMatcher:ExampleMatcher有关于匹配特定字段的匹配规则,他可以重复在多个实例中。必填,如果不填为默认的
  • Example:Examle由探针和ExampleMatcher组成的。它用于创建查询。

QueryByExampleExecutor示例

public interface Example<T> {
    static <T> Example<T> of(T probe) {
        return new TypedExample(probe, ExampleMatcher.matching());
    }
 
    static <T> Example<T> of(T probe, ExampleMatcher matcher) {
        return new TypedExample(probe, matcher);
    }
 
    T getProbe();
 
    ExampleMatcher getMatcher();
 
    default Class<T> getProbeType() {
        return ProxyUtils.getUserClass(this.getProbe().getClass());
    }
}

//创建查询条件数据对象
Customer customer = new Customer();
customer.setName("Jack");
customer.setAddress("上海");
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
        .withMatcher("name", GenericPropertyMatchers.startsWith()) //姓名采用“开始匹配”的方式查询
        .withIgnorePaths("focus");  //忽略属性:是否关注。因为是基本类型,需要忽略掉
//创建实例
Example<Customer> ex = Example.of(customer, matcher); 
//查询
List<Customer> ls = dao.findAll(ex);
//输出结果
for (Customer bo:ls)
{
    System.out.println(bo.getName());
}


上面例子中,是这样创建“实例”的:Example<Customer> ex = Example.of(customer, matcher);可以看到,Example 对象由 customer 和 matcher 共同创建,为讲解方便,再来结合案例先来明确一些定义。

  • Probe:实体对象,在持久化框架中与 Table 对应的域对象,一个对象代表数据库表中的一条记录,如上例中 Customer 对象。在构建查询条件时,一个实体对象代表的是查询条件中的“数值”部分,如要查询姓“Jack”的客户,实体对象只能存储条件值“Jack”。
  • ExampleMatcher:匹配器,它是匹配“实体对象”的,表示了如何使用“实体对象”中的“值”进行查询,它代表的是“查询方式”,解释了如何去查的问题。例如,要查询姓“刘”的客户,即姓名以“刘”开头的客户,该对象就表示了“以某某开头的”这个查询方式,如上例中 withMatcher("name", GenericPropertyMatchers.startsWith())。
  • Example:实例对象,代表的是完整的查询条件。由实体对象(查询条件值)和匹配器(查询方式)共同创建。

再来理解“实例查询”,顾名思义,就是通过一个例子来查询,要查询的是 Customer 对象,查询条件也是一个 Customer 对象,通过一个现有的客户对象作为例子,查询和这个例子相匹配的对象。

QueryByExampleExecutor 的特点及约束

  • 支持动态查询:即支持查询条件个数不固定的情况,如客户列表中有多个过滤条件,用户使用时在“地址”查询框中输入了值,就需要按地址进行过滤,如果没有输入值,就忽略这个过滤条件。对应的实现是,在构建查询条件 Customer 对象时,将 address 属性值置具体的条件值或置为 null。
  • 不支持过滤条件分组:即不支持过滤条件用 or(或)来连接,所有的过滤查件,都是简单一层的用 and(并且)连接,如 firstname = ?0 or (firstname = ?1 and lastname = ?2)。
  • 仅支持字符串的开始/包含/结束/正则表达式匹配和其他属性类型的精确匹配。查询时,对一个要进行匹配的属性(如:姓名 name),只能传入一个过滤条件值,如以 Customer 为例,要查询姓“刘”的客户,“刘”这个条件值就存储在表示条件对象的 Customer 对象的 name 属性中,针对于“姓名”的过滤也只有这么一个存储过滤值的位置,没办法同时传入两个过滤值。正是由于这个限制,有些查询是没办法支持的,例如要查询某个时间段内添加的客户,对应的属性是 addTime,需要传入“开始时间”和“结束时间”两个条件值,而这种查询方式没有存两个值的位置,所以就没办法完成这样的查询。

ExampleMatcher 源码解读

public class ExampleMatcher {
NullHandler nullHandler; 
StringMatcher defaultStringMatcher; //默认
boolean defaultIgnoreCase; //默认大小写忽略方式
PropertySpecifiers propertySpecifiers; //各属性特定查询方式
Set<String> ignoredPaths; //忽略属性列表
//Null值处理方式,通过构造方法,我们发现默认忽略
   NullHandler nullHandler;
   //字符串匹配方式,通过构造方法可以看出默认是DEFAULT(默认,效果同EXACT),EXACT(相等)
   StringMatcher defaultStringMatcher;
   //各属性特定查询方式,默认无特殊指定的。
   PropertySpecifiers propertySpecifiers;
   //忽略属性列表,默认无。
   Set<String> ignoredPaths;
   //大小写忽略方式,默认不忽略。
   boolean defaultIgnoreCase;
   @Wither(AccessLevel.PRIVATE) MatchMode mode;
//通用、内部、默认构造方法。
   private ExampleMatcher() {
      this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections.<String>emptySet(), false,
            MatchMode.ALL);
   }
   //Example的默认匹配方式
   public static ExampleMatcher matching() {
      return matchingAll();
   }
public static ExampleMatcher matchingAll() {
   return new ExampleMatcher().withMode(MatchMode.ALL);
}
......
}


关键属性分析

  • nullHandler:Null 值处理方式,枚举类型,有两个可选值,INCLUDE(包括)、IGNORE(忽略)。
  1. 标识作为条件的实体对象中,一个属性值(条件值)为 Null 时,是否参与过滤;
  2. 当该选项值是 INCLUDE 时,表示仍参与过滤,会匹配数据库表中该字段值是 Null 的记录;
  3. 若为 IGNORE 值,表示不参与过滤。
  • defaultStringMatcher:默认字符串匹配方式,枚举类型,有 6 个可选值,DEFAULT(默认,效果同 EXACT)、EXACT(相等)、STARTING(开始匹配)、ENDING(结束匹配)、CONTAINING(包含,模糊匹配)、REGEX(正则表达式)。
  1. 该配置对所有字符串属性过滤有效,除非该属性在 propertySpecifiers 中单独定义自己的匹配方式。
  • defaultIgnoreCase:默认大小写忽略方式,布尔型,当值为 false 时,即不忽略,大小不相等。
  1. 该配置对所有字符串属性过滤有效,除非该属性在 propertySpecifiers 中单独定义自己的忽略大小写方式。
  • propertySpecifiers:各属性特定查询方式,描述了各个属性单独定义的查询方式,每个查询方式中包含4个元素:属性名、字符串匹配方式、大小写忽略方式、属性转换器。
  1. 如果属性未单独定义查询方式,或单独查询方式中,某个元素未定义(如字符串匹配方式),则采用 ExampleMatcher 中定义的默认值,即上面介绍的 defaultStringMatcher 和 defaultIgnoreCase 的值。
  • ignoredPaths:忽略属性列表,忽略的属性不参与查询过滤。

(3)字符串匹配举例

字符串匹配方式       对应 JPQL 的写法
Default& 不忽略大小写   firstname=?1
Exact& 忽略大小写   LOWER(firstname) = LOWER(?1)
Staring& 忽略大小写  LOWER(firstname) like LOWER(?0)+'%'
Ending& 不忽略大小写  firstname like '%'+?1
Containing 不忽略大小写 firstname like '%'+?1+'%'


QueryByExampleExecutor 使用场景 & 实际的使用

使用场景

使用一组静态或动态约束来查询数据存储、频繁重构域对象,而不用担心破坏现有查询、简单的查询的使用场景,有时候还是挺方便的。

实际使用中我们需要考虑的因素

查询条件的表示,有两部分,一是条件值,二是查询方式。条件值用实体对象(如 Customer 对象)来存储,相对简单,当页面传入过滤条件值时,存入相对应的属性中,没入传入时,属性保持默认值。查询方式是用匹配器 ExampleMatcher 来表示,情况相对复杂些,需要考虑的因素有以下几个:

(1)Null 值的处理

当某个条件值为 Null时,是应当忽略这个过滤条件呢,还是应当去匹配数据库表中该字段值是 Null 的记录?

Null 值处理方式:默认值是 IGNORE(忽略),即当条件值为 Null 时,则忽略此过滤条件,一般业务也是采用这种方式就可满足。当需要查询数据库表中属性为 Null 的记录时,可将值设为 INCLUDE,这时,对于不需要参与查询的属性,都必须添加到忽略列表(ignoredPaths)中,否则会出现查不到数据的情况。

(2)基本类型的处理

如客户 Customer 对象中的年龄 age 是 int 型的,当页面不传入条件值时,它默认是0,是有值的,那是否参与查询呢?

关于基本数据类型处理方式:实体对象中,避免使用基本数据类型,采用包装器类型。如果已经采用了基本类型,而这个属性查询时不需要进行过滤,则把它添加到忽略列表(ignoredPaths)中。

(3)忽略某些属性值

一个实体对象,有许多个属性,是否每个属性都参与过滤?是否可以忽略某些属性?

ignoredPaths:虽然某些字段里面有值或者设置了其他匹配规则,只要放在 ignoredPaths 中,就会忽略此字段的,不作为过滤条件。

(4)不同的过滤方式

同样是作为 String 值,可能“姓名”希望精确匹配,“地址”希望模糊匹配,如何做到?

默认配置和特殊配置混合使用:默认创建匹配器时,字符串采用的是精确匹配、不忽略大小写,可以通过操作方法改变这种默认匹配,以满足大多数查询条件的需要,如将“字符串匹配方式”改为 CONTAINING(包含,模糊匹配),这是比较常用的情况。对于个别属性需要特定的查询方式,可以通过配置“属性特定查询方式”来满足要求,设置 propertySpecifiers 的值即可。

(5)大小写匹配

字符串匹配时,有时可能希望忽略大小写,有时则不忽略,如何做到?

defaultIgnoreCase:忽略大小的生效与否,是依赖于数据库的。例如 MySQL 数据库中,默认创建表结构时,字段是已经忽略大小写的,所以这个配置与否,都是忽略的。如果业务需要严格区分大小写,可以改变数据库表结构属性来实现。

实际使用案例说明

(1)无匹配器的情况

  • 要求:查询地址是“河南省郑州市”,且重点关注的客户。
  • 说明:使用默认匹配器就可以满足查询条件,则不需要创建匹配器。
//创建查询条件数据对象
Customer customer = new Customer();
customer.setAddress("河南省郑州市");
customer.setFocus(true);
//创建实例
Example<Customer> ex = Example.of(customer); 
//查询
List<Customer> ls = dao.findAll(ex);

(2)多种条件组合

  • 要求:根据姓名、地址、备注进行模糊查询,忽略大小写,地址要求开始匹配。
  • 说明:这是通用情况,主要演示改变默认字符串匹配方式、改变默认大小写忽略方式、属性特定查询方式配置、忽略属性列表配置。
//创建查询条件数据对象
Customer customer = new Customer();
customer.setName("zhang");
customer.setAddress("河南省");
customer.setRemark("BB");
//虽然有值,但是不参与过滤条件
customer.setFocus(true);
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
        .withStringMatcher(StringMatcher.CONTAINING) //改变默认字符串匹配方式:模糊查询
        .withIgnoreCase(true) //改变默认大小写忽略方式:忽略大小写
        .withMatcher("address", GenericPropertyMatchers.startsWith()) //地址采用“开始匹配”的方式查询
        .withIgnorePaths("focus");  //忽略属性:是否关注。因为是基本类型,需要忽略掉
//创建实例
Example<Customer> ex = Example.of(customer, matcher); 
//查询
List<Customer> ls = dao.findAll(ex);


(3)多级查询

  • 要求:查询所有潜在客户。
  • 说明:主要演示多层级属性查询。
//创建查询条件数据对象
CustomerType type = new CustomerType();
type.setCode("01"); //编号01代表潜在客户
Customer customer = new Customer();
customer.setCustomerType(type);        
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
        .withIgnorePaths("focus");  //忽略属性:是否关注。因为是基本类型,需要忽略掉                
//创建实例
Example<Customer> ex = Example.of(customer, matcher); 
//查询
List<Customer> ls = dao.findAll(ex);


(4)查询 Null 值

  • 要求:地址是 Null 的客户。
  • 说明:主要演示改变“Null 值处理方式”。
//创建查询条件数据对象
Customer customer = new Customer();
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
        //改变“Null值处理方式”:包括。
      .withIncludeNullValues() 
       //忽略其他属性
      .withIgnorePaths("id", "name", "sex", "age", "focus", "addTime", "remark", "customerType"); 
//创建实例
Example<Customer> ex = Example.of(customer, matcher);
//查询
List<Customer> ls = dao.findAll(ex);


(5)虽然我们工作中用的最多的还是“简单查询”(因为简单,所以…)和基于 JPA Criteria 的动态查询(可以满足所有需求,没有局限性)。

但是 QueryByExampleExecutor 还是个非常不错的两种中间场景的查询处理手段,其他人没有用,感觉是对其不熟悉,还是希望我们学习过 QueryByExampleExecutor 的开发者用起来,用熟悉了会增加开发效率。

JpaSpecificationExecutor的使用

JpaSpecificationExecutor是JPA2.0提供的Criteria API,可以用于动态生成query。Spring Data JPA支持Criteria查询,可以很方便地使用,足以应付工作中的所有复杂查询情况,可以对JPA最大限度的扩展。

public interface JpaSpecificationExecutor<T> {
        // 根据Specification条件查询单个对象
    Optional<T> findOne(@Nullable Specification<T> var1);
        // 根据Specification条件查询List结果
    List<T> findAll(@Nullable Specification<T> var1);
        // 根据Specification条件,分页查询
    Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);
        // 根据Specification条件,带排序地查询结果
    List<T> findAll(@Nullable Specification<T> var1, Sort var2);
        // 根据Specification条件,查询数量
    long count(@Nullable Specification<T> var1);
}


这个接口基本围绕Specification接口来定义的,Specification接口自定义了如下方法。

/**
 * Specifications 是 Spring Data JPA 对 Specification 的聚合操作工具类,里面有以下四个方法:
*/
@Deprecated  //已经不推荐使用了,我们可以用 Specification 来代替,如上图。
public class Specifications<T> implements Specification<T>, Serializable {
   private final Specification<T> spec;
   //构造方法私有化,只能通过 where/not 创建 Specifications 对象。
   private Specifications(Specification<T> spec) {
      this.spec = spec;
   }
   //创建 where 后面的 Predicate 集合
   public static <T> Specifications<T> where(Specification<T> spec) {
      return new Specifications<T>(spec);
   }
   //创建 not 集合的 Predicate
   public static <T> Specifications<T> not(Specification<T> spec) {
      return new Specifications<T>(new NegatedSpecification<T>(spec));
   }
   //Specification 的 and 关系集合
   public Specifications<T> and(Specification<T> other) {
      return new Specifications<T>(new ComposedSpecification<T>(spec, other, AND));
   }
   //Specification 的 or 关系集合
   public Specifications<T> or(Specification<T> other) {
      return new Specifications<T>(new ComposedSpecification<T>(spec, other, OR));
   }
......
}


而如果查看 Specifications 源码的话就会发现,其已经将来要被删除了,已经不推荐使用了,而另外两个都是局部私有的,所以真正关注的就是 Specification 接口中如下一个接口方法

public interface Specification<T> {
   Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}


从这里可以看出,每个调用的地方都需要,创建 Specification 的实现类,而 JpaSpecificationExecutor 是针对 Criteria API 进行了 predicate 标准封装,帮我们封装了通过 EntityManager 的查询和使用细节,使操作 Criteria 更加便利了一些。所以我们要掌握一下 Predicate、Root、CriteriaQuery、CriteriaBuilder 是什么?

Criteria 的概念简单介绍

Root<T> root

代表了可以查询和操作的实体对象的根,如果将实体对象比喻成表名,那 root 里面就是这张表里面的字段,这不过是 JPQL 的实体字段而已。通过里面的 Path get(String attributeName),来获得我们想操作的字段。

CriteriaQuery<?> query

代表一个 specific 的顶层查询对象,它包含着查询的各个部分,比如 select 、from、where、group by、order by 等。CriteriaQuery 对象只对实体类型或嵌入式类型的 Criteria 查询起作用,简单理解,它提供了查询 ROOT 的方法。常用的方法有:

CriteriaQuery<T> where(Predicate... restrictions);
CriteriaQuery<T> select(Selection<? extends T> selection);
CriteriaQuery<T> having(Predicate... restrictions);
CriteriaBuilder cb

用来构建 CritiaQuery 的构建器对象,其实就相当于条件或者是条件组合,并以 Predicate 的形式返回。下面是构建简单的 Predicate 示例:

Predicate p1=cb.like(root.get(“name”).as(String.class), “%”+uqm.getName()+“%”);
Predicate p2=cb.equal(root.get("uuid").as(Integer.class), uqm.getUuid());
Predicate p3=cb.gt(root.get("age").as(Integer.class), uqm.getAge());


构建组合的 Predicate 示例:

Predicate p = cb.and(p3,cb.or(p1,p2));


实际经验

到此我们发现其实 JpaSpecificationExecutor 帮我提供了一个高级的入口和结构,通过这个入口,可以使用底层 JPA 的 Criteria 所有方法,其实就可以满足了所有业务场景。但在实际工作中,需要注意的是,如果一旦我们写的实现逻辑太复杂,第二个人看不懂时,那一定是有问题的,我要寻找更简单的、更易懂的、更优雅的方式。比如:

  • 分页和排序我们就没有比较自己再去实现一遍逻辑,直接用其开放的 Pageable 和 Sort 即可。
  • 当我们过多的使用 group 或者 having、sum、count 等内置的 SQL 函数的时候,我们想想就是通过 Specification 实现了逻辑,这种效率真的高吗?是不是数据在其他算好更好?
  • 当我们过多的操作 left join 和 inner Join 的链表查询的时候,我们想想,是不是通过数据库的视图(view)更优雅一点?


JpaSpecificationExecutor 使用案例

1、新建两个实体

@Entity(name = "UserInfoEntity")
@Table(name = "user_info", schema = "test")
public class UserInfoEntity  implements Serializable {
   @Id
   @Column(name = "id", nullable = false)
   private Integer id;
   @Column(name = "first_name", nullable = true, length = 100)
   private String firstName;
   @Column(name = "last_name", nullable = true, length = 100)
   private String lastName;
   @Column(name = "telephone", nullable = true, length = 100)
   private String telephone;
   @Column(name = "create_time", nullable = true)
   private Date createTime;
   @Column(name = "version", nullable = true)
   private String version;
   @OneToOne(optional = false,fetch = FetchType.EAGER)
   @JoinColumn(referencedColumnName = "id",name = "address_id",nullable = false)
   @Fetch(FetchMode.JOIN)
   private UserReceivingAddressEntity addressEntity;
......
}
@Entity
@Table(name = "user_receiving_address", schema = "test")
public class UserReceivingAddressEntity  implements Serializable {
   @Id
   @Column(name = "id", nullable = false)
   private Integer id;
   @Column(name = "user_id", nullable = false)
   private Integer userId;
   @Column(name = "address_city", nullable = true, length = 500)
   private String addressCity;
......
}


2、UserRepository 需要继承 JpaSpecificationExecutor

public interface UserRepository extends JpaSpecificationExecutor<UserInfoEntity> {
}


3、调用者 UserInfoManager 的写法

  • 我们演示一下直接用 lambda 使用 Root 和 CriteriaBuilder 做一个简单的不同条件的查询和链表查询。
@Component
public class UserInfoManager {
   @Autowired
   private UserRepository userRepository;
   public Page<UserInfoEntity> findByCondition(UserInfoRequest userParam,Pageable pageable){
      return userRepository.findAll((root, query, cb) -> {
         List<Predicate> predicates = new ArrayList<Predicate>();
         if (StringUtils.isNoneBlank(userParam.getFirstName())){
            //liked的查询条件
            predicates.add(cb.like(root.get("firstName"),"%"+userParam.getFirstName()+"%"));
         }
         if (StringUtils.isNoneBlank(userParam.getTelephone())){
            //equal查询条件
            predicates.add(cb.equal(root.get("telephone"),userParam.getTelephone()));
         }
         if (StringUtils.isNoneBlank(userParam.getVersion())){
            //greaterThan大于等于查询条件
            predicates.add(cb.greaterThan(root.get("version"),userParam.getVersion()));
         }
         if (userParam.getBeginCreateTime()!=null&&userParam.getEndCreateTime()!=null){
            //根据时间区间去查询   predicates.add(cb.between(root.get("createTime"),userParam.getBeginCreateTime(),userParam.getEndCreateTime()));
         }
         if (StringUtils.isNotBlank(userParam.getAddressCity())) {
            //联表查询,利用root的join方法,根据关联关系表里面的字段进行查询。
            predicates.add(cb.equal(root.join("addressEntityList").get("addressCity"), userParam.getAddressCity()));
         }
         return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
      }, pageable);
   }
}
//可以仔细体会上面这个案例,实际工作中应该大部分都是这种写法,就算扩展也是百变不离其中。
  • 我们再来看一个不常见的复杂查询的写法,来展示一下 CriteriaQuery 的用法
public List<MessageRequest> findByConditions(String name, Integer price, Integer stock) {  
        messageRequestRepository.findAll((Specification<MessageRequest>) (itemRoot, query, criteriaBuilder) -> {
            //这里用 List 存放多种查询条件,实现动态查询
            List<Predicate> predicatesList = new ArrayList<>();
            //name 模糊查询,like 语句
            if (name != null) {
                predicatesList.add(
                    criteriaBuilder.and(
                        criteriaBuilder.like(
                            itemRoot.get("name"), "%" + name + "%")));
            }
            // itemPrice 小于等于 <= 语句
            if (price != null) {
                predicatesList.add(
                    criteriaBuilder.and(
                        criteriaBuilder.le(
                            itemRoot.get("price"), price)));
            }
            //itemStock 大于等于 >= 语句
            if (stock != null) {
                predicatesList.add(
                    criteriaBuilder.and(
                        criteriaBuilder.ge(
                            itemRoot.get("stock"), stock)));
            }
            //where() 拼接查询条件
            query.where(predicatesList.toArray(new Predicate[predicatesList.size()]));
            //返回通过 CriteriaQuery 拼装的 Predicate
            return query.getRestriction();
        });
    }
  • 而没有 Spring Data JPA 封装之前,如果想获得此三个对象 Root root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder,老式 Hibernate 的写法如下(PS:强烈不推荐哦,虽然现在也支持,只是让大家知道了解一下。):
@Autowired //导入entityManager
 private EntityManager entityManager;
//创建CriteriaBuilder安全查询工厂,CriteriaBuilder是一个工厂对象,安全查询的开始.用于构建JPA安全查询.
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
//创建CriteriaQuery安全查询主语句
//CriteriaQuery对象必须在实体类型或嵌入式类型上的Criteria 查询上起作用。
CriteriaQuery<Item> query = criteriaBuilder.createQuery(Item.class);
//Root 定义查询的From子句中能出现的类型
Root<Item> itemRoot = query.from(Item.class);

  • 我们再来看一个利用 CriteriaQuery 例子,其实大家可以扩展一下思路,就是 Hibernate 那套在这里面都支持,不过作者还是建议代码越简单越好。
List<UserSpuFavoriteEntity> result = userSpuFavoriteDao.findAll((Root<UserSpuFavoriteEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb)->{
            query.where(cb.and(cb.equal(root.get("userName"), userName),cb.isFalse(root.get("isDelete"))));
            query.orderBy(cb.desc(root.get("updateTime")));
            return query.getRestriction();
        });


Specification 工作中的一些扩展

我们在实际工作中会发现,如果上面的逻辑,简单重复写总感觉是不是可以抽出一些公用方法呢,此时引入一种工厂模式,帮我们做一些事情,可以让代码更加优雅。基于 JpaSpecificationExecutor 的思路,我们创建一个 SpecificationFactory.Java 内容如下:

public final class SpecificationFactory {
   /**
    * 模糊查询,匹配对应字段
    */
   public static Specification containsLike(String attribute, String value) {
      return (root, query, cb)-> cb.like(root.get(attribute), "%" + value + "%");
   }
   /**
    * 某字段的值等于 value 的查询条件
    */
   public static Specification equal(String attribute, Object value) {
      return (root, query, cb) -> cb.equal(root.get(attribute),value);
   }
   /**
    * 获取对应属性的值所在区间
    */
   public static Specification isBetween(String attribute, int min, int max) {
      return (root, query, cb) -> cb.between(root.get(attribute), min, max);
   }
   public static Specification isBetween(String attribute, double min, double max) {
      return (root, query, cb) -> cb.between(root.get(attribute), min, max);
   }
   public static Specification isBetween(String attribute, Date min, Date max) {
      return (root, query, cb) -> cb.between(root.get(attribute), min, max);
   }
   /**
    * 通过属性名和集合实现 in 查询
    */
   public static Specification in(String attribute, Collection c) {
      return (root, query, cb) ->root.get(attribute).in(c);
   }
   /**
    * 通过属性名构建大于等于 Value 的查询条件
    */
   public static Specification greaterThan(String attribute, BigDecimal value) {
      return (root, query, cb) ->cb.greaterThan(root.get(attribute),value);
   }
   public static Specification greaterThan(String attribute, Long value) {
      return (root, query, cb) ->cb.greaterThan(root.get(attribute),value);
   }
......
}

PS:可以根据实际工作需要和场景进行不断扩充。

调用实例1:

userRepository.findAll(
      SpecificationFactory.containsLike("firstName", userParam.getLastName()),
      pageable);


是不是发现代码一下子少了很多?

调用实例2:

userRepository.findAll(Specifications.where(
      SpecificationFactory.containsLike("firstName", userParam.getLastName()))
            .and(SpecificationFactory.greaterThan("version",userParam.getVersion())),
      pageable);


和我们前面举的例子比起来是不是代码更加优雅、可读性更加强了?

JpaSpecificationExecutor 实现原理

我们还是先通过开发工具,把关键的类添加到Diagram上面进行分析,如图:

dc05ed77aea16f8652949960949d6c15.png
我们通过上图可以看一下,前面介绍的几个类之间的关联关系。

SimpleJpaRepository 实现类中的关键源码如下:

/**
 *以 findOne 为例
*/
public T findOne(Specification<T> spec) {
   try {
      return getQuery(spec, (Sort) null).getSingleResult();
   } catch (NoResultException e) {
      return null;
   }
}
/*
 * 解析 Specification,利用 EntityManager 直接实现调用逻辑。
*/
protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) {
   CriteriaBuilder builder = em.getCriteriaBuilder();
   CriteriaQuery<S> query = builder.createQuery(domainClass);
   Root<S> root = applySpecificationToCriteria(spec, domainClass, query);
   query.select(root);
   if (sort != null) {
      query.orderBy(toOrders(sort, root, builder));
   }
   return applyRepositoryMethodMetadata(em.createQuery(query));
}


其实我们可以看的出来底层都是调用的 EntityManager。

与 EntityManager 的关系图

0ee5bc77d2731c1b2586f2b00c39a731.png
通过此图可以体会一下 Repository 和 EntityManager 的关联关系。

自定义 JpaRepository 简介

由于业务场景的千差万别,有可能需要定义自己的 Repository 类,其实通过上面的章节,我们也大概能想到 Spring Data JPA 可以轻松地允许你提供自定义 Repository,并且还很容易与现有的抽象和查询方法集成。

EntityManager 介绍

我们前面已经无数次提到了,JPA 的默认 Repository 的实现类是 SimpleJpaRepository,而里面的具体实现就是调用的 EntityManager。对于 javax.persistence.EntityManager 通过源码,先来看下它主要给我们提供了哪几个方法:

public interface EntityManager {
  /**
    *根据主键查询实体对象
    */
  public <T> T find(Class<T> entityClass, Object primaryKey);
    /**
     *  支持JQPL的语法
     * @param qlString a Java Persistence query string
     */
    public Query createQuery(String qlString);
    /**
     * 利用CriteriaQuery来创建查询
     * @param criteriaQuery  a criteria query object
     */
    public <T> TypedQuery<T> createQuery(CriteriaQuery<T> criteriaQuery);
    /**
     * 利用CriteriaUpdate创建更新查询
     * @param updateQuery a criteria update query object
     */
    public Query createQuery(CriteriaUpdate updateQuery);
    /**
     * 利用CriteriaDelete创建删除查询
     * @param deleteQuery a criteria delete query object
     */
    public Query createQuery(CriteriaDelete deleteQuery);
    /**
     * 利用原生的sql语句创建查询,可以是查询、更新、删除等sql
     * @param sqlString a native SQL query string
     */
    public Query createNativeQuery(String sqlString);
    /**
     * 利用原生SQL查询,指定返回结果类型
     * @param sqlString a native SQL query string
     * @param resultClass the class of the resulting instance(s)
     */
    public Query createNativeQuery(String sqlString, Class resultClass);
......
}


EntityManager 的简单使用案例

案例1:针对复杂的原生 SQL 的查询

//创建sql语句
StringBuilder querySQL = new StringBuilder("SELECT spu_id AS spuId ,spu_name AS spuName,")
        .append("SUM(system_price_count) AS systemPriceCount,")
        .append("SUM(wechat_applet_view_count) AS wechatAppletViewCount")
        .append(" FROM report_spu_summary ");
//利用entityManager实现查询
Query query = entityManager.createNativeQuery(querySQL.toString() + whereSQL.toString() + groupBy + orderBy.toString());
//分页
query.setFirstResult(custom.offset()).setMaxResults(custom.getPageSize());
//结果转换
query.unwrap(SQLQuery.class).setResultTransformer(Transformers.aliasToBean(ReportSpuSummarySumBo.class));
//得到最终的返回结果
List<ReportSpuSummarySumBo> results = query.getResultList();


此案例仅仅为了说明 entityManager.createNativeQuery 的查询方法,但是不推荐用这种用法,开发思路可转换一下,做到心中有数即可。

案例2:find 方法

entityManager.find(UserInfoEntity.class,1);


案例3:JPQL 的用法

Query query = entityManager.createQuery("SELECT c FROM Customer c");  
List<Customer> result = query.getResultList();


EntityManager 使用起来还是比较简单的。

自定义实现 Repository

案例1:单个私有的 Repository 接口实现类

(1)创建自定义接口

/**
 * @author jack
 */
public interface UserRepositoryCustom {
    /**
     * 自定义一个查询方法,name的like查询,此处仅仅是演示例子,实际中直接用QueryMethod即可
     * @param firstName
     * @return
     */
    List<User> customerMethodNamesLike(String firstName);
}


(2)自定义存储库功能的实现

/**
 * 用@Repository 将此实现交个Spring bean加载
 * 咱们模仿SimpleJpaRepository 默认将所有方法都开启一个事务
 */
@Repository
@Transactional(readOnly = true)
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
    @PersistenceContext
    EntityManager entityManager;
    /**
     * 自定义一个查询firstname的方法
     * @param firstName
     * @return
     */
    @Override
    public List<User> customerMethodNamesLike(String firstName) {
        Query query = entityManager.createNativeQuery("SELECT u.* FROM user as u " +
                "WHERE u.name LIKE ?", User.class);
        query.setParameter(1, firstName + "%");
        return query.getResultList();
    }
}


(3)我们这里采用 entityManager,当然了也不排除自己通过最底层的 JdbcTemplate 来自己实现逻辑。

(4)由于这个接口是为 User 单独写的,但是同时也可以继承和 @Repository 的任何子类。

/**
 * 使用的时候直接继承 UserRepositoryCustom接口即可
 */
public interface UserRepository extends Repository<User, Long>,UserRepositoryCustom {
}


案例2:定义一个公用的 Repository 接口的实现类。

通过构造方法获得 EntityManager,需要用到 Java 的泛化技术。当你想将一个方法添加到所有的存储库接口时,上述方法是不可行的,要将自定义行为添加到所有存储库,首先添加一个中间接口来声明共享行为。

(1)声明定制共享行为的接口,用 @NoRepositoryBean:

//因为要公用,所以必须要通用,不能失去本身的Spring Data JPA给我们提供的默认方法,所有我们继承相关的Repository类
@NoRepositoryBean
public interface MyRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID> {
  void sharedCustomMethod(ID id);
}


(2)继承 SimpleJpaRepository 扩展自己的方法实现逻辑:

public class MyRepositoryImpl<T, ID extends Serializable>
  extends SimpleJpaRepository<T, ID> implements MyRepository<T, ID> {
  private final EntityManager entityManager;
  public MyRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) {
    super(entityInformation, entityManager);
    // Keep the EntityManager around to used from the newly introduced methods.
    this.entityManager = entityManager;
  }
  public void sharedCustomMethod(ID id) {
    // 通过entityManager实现自己的额外方法的实现逻辑。这里不多说了
  }
}

注意:该类需要具有专门的存储库工厂实现使用超级类的构造函数,如果存储库基类有多个构造函数,则覆盖一个 EntityInformation 加上特定于存储的基础架构对象(例如,一个 EntityManager 或一个模板类),也可以重写 SimpleJpaRepository 的任何逻辑。如逻辑删除放在这里面实现,就不要所有的 Repository 去关心实现哪个接口了。

(3)使用 JavaConfig 配置自定义 MyRepositoryImpl 作为其他接口的动态代理的实现基类。

具有全局的性质,即使没有继承它所有的动态代理类也会变成它。

@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }


实际工作的应用场景总结及其案例
在实际工作中,有哪些场景会用到自定义 Repository 呢,这里列出几种实际在工作中的应用案例。

1、逻辑删除场景

可以用到上面说的两种实现方式,如果有框架级别的全局自定义 Respository 那就在全局实现里面覆盖默认 remove 方法,这样就会统一全部只能使用逻辑删除。但是一般是自定义一个特殊的删除Respository,让大家去根据不同的domain业务逻辑去选择使用此接口即可。

2、当有业务场景要覆盖 SimpleJpaRepository 默认实现的时候

这种一般是具体情况具体分析的,一般实现特殊化的自定义 Respository 即可。

3、UUID 与 ID 的情况

经常在实际生产中会有这样的场景,对外暴露的是 UUID 查询方法,而对内呢暴露的是 Long 类型的 ID,这时候我们就可以自定义一个 FindByIDOrUUID 的底层实现方法,在自定义的 Respository 接口里面。

4、使用 Querydsl

Spring Data JPA 里面还帮我们做了 QuerydslJpaRepository 用来支持 Querydsl 的查询方法,当我们引入 Querydsl 的时候 Spring 就会自动帮我们把 SimpleJpaRepository 的实现切换到 QuerydslJpaRepository 的实现。

5、动态查询条件

由于 Data JPA 里面的 query method 或者 @query 注解不支持动态查询条件,正常情况下将动态条件写在 manager 或者 service 里面。这个时候如果是针对资源的操作,并且和业务无关的查询的时候可以放在自定义 Repository 里面(有个缺点就是不能使用 SimpleJpaRepository,里面的很多优秀的默认是实现方法,在实际工作中还是放在 service 和 manager 中多一些,只是给大家举个例子,知道有这么回事就行)。实例如下:

//我们假设要根据条件动态查询订单
public interface OrderRepositoryCustom {
    Page<Order> findAllByCriteria(OrderCriteria criteria); // 定义一个订单的定制化Repository查询方法,当然实际生产过程中,这里面可能不止一个方法。
}
public class OrderRepositoryImpl implements OrderRepositoryCustom { 
    @PersistenceContext
    EntityManager entityManager; 
    /**
    * 一个动态条件的查询方法
    */
    public List<Order> findAllByCriteria(OrderCriteria criteria) {
        // 查询条件列表
        final List<String> andConditions = new ArrayList<String>();
        final Map<String, Object> bindParameters = new HashMap<String, Object>();
        // 动态绑定参数和要查询的条件
        if (criteria.getId() != null) {
            andConditions.add("o.id = :id");
            bindParameters.put("id", criteria.getId());
        }
        if (!CollectionUtils.isEmpty(criteria.getStatusCodes())) {
            andConditions.add("o.status.code IN :statusCodes");
            bindParameters.put("statusCodes", criteria.getStatusCodes());
        }
        if (andConditions.isEmpty()) {
            return Collections.emptyList();
        }
        // 动态创建query
        final StringBuilder queryString = new StringBuilder();
        queryString.append("SELECT o FROM Order o");
        // 动态拼装条件
        Iterator<String> andConditionsIt = andConditions.iterator();
        if (andConditionsIt.hasNext()) {
            queryString.append(" WHERE ").append(andConditionsIt.next());
        }
        while (andConditionsIt.hasNext()) {
            queryString.append(" AND ").append(andConditionsIt.next());
        }
        // 添加排序
        queryString.append(" ORDER BY o.id");
        // 创建 typed query.
        final TypedQuery<Order> findQuery = entityManager.createQuery(
                queryString.toString(), Order.class);
        // 绑定参数
        for (Map.Entry<String, Object> bindParameter : bindParameters
                .entrySet()) {
            findQuery.setParameter(bindParameter.getKey(), bindParameter
                    .getValue());
        }
        //返回查询,结果。
        return findQuery.getResultList();
    }
}
//实际中此种就比较少用了,大家知道有这么回事,真是遇到特殊场景必须要用了,可以用此方法实现。


6、扩展 JpaSpecificationExecutor 使其更加优雅

当我们动态查询的时候经常会出现下面的代码逻辑,写起来老是感觉有点不是特别优雅,且有点重复的感觉:

PageRequest pr = new PageRequest(page - 1, rows, Direction.DESC, "id");
    Page pageData = memberDao.findAll(new Specification() {
        @Override
        public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
            List<Predicate> predicates = new ArrayList<>();
            if (isNotEmpty(userName)) {
                predicates.add(cb.like(root.get("userName"), "%" + userName + "%"));
            }
            if (isNotEmpty(realName)) {
                predicates.add(cb.like(root.get("realName"), "%" + realName + "%"));
            }
            if (isNotEmpty(telephone)) {
                predicates.add(cb.equal(root.get("userName"), telephone));
            }
            query.where(predicates.toArray(new Predicate[0]));
            return null;
        }
    }, pr);


使用了自定义的复杂查询,我们可以做到如下效果:

Page pageData = userDao.findAll(new MySpecification<User>().and(
        Cnd.like("userName", userName),
        Cnd.like("realName", realName),
        Cnd.eq("telephone", telephone)
).asc("id"), pr);


如果对 Spring MVC 比较熟悉的话,可以更进一步把其查询提交和规则直接封装到 HandlerMethodArgumentResolver 里面,把参数自动和规则匹配起来。

我们可以对如下代码进行参考,感觉实现的还不错,此段代码可以作参考,只是实现的还有点不完整,如下:

/**
 * 扩展Specification
 * @param <T>
 */
public class MySpecification<T> implements Specification<T> {
    /**
     * 属性分隔符
     */
    private static final String PROPERTY_SEPARATOR = ".";
    /**
     * and条件组
     */
    List<Cnd> andConditions = new ArrayList<>();
    /**
     * or条件组
     */
    List<Cnd> orConditions = new ArrayList<>();
    /**
     * 排序条件组
     */
    List<Order> orders = new ArrayList<>();
    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
        Predicate restrictions = cb.and(getAndPredicates(root, cb));
        restrictions = cb.and(restrictions, getOrPredicates(root, cb));
        cq.orderBy(getOrders(root, cb));
        return restrictions;
    }
    public MySpecification and(Cnd... conditions) {
        for (Cnd condition : conditions) {
            andConditions.add(condition);
        }
        return this;
    }
    public MySpecification or(Collection<Cnd> conditions) {
        orConditions.addAll(conditions);
        return this;
    }
    public MySpecification desc(String property) {
        this.orders.add(Order.desc(property));
        return this;
    }
    public MySpecification asc(String property) {
        this.orders.add(Order.asc(property));
        return this;
    }
    private Predicate getAndPredicates(Root<T> root, CriteriaBuilder cb) {
        Predicate restrictions = cb.conjunction();
        for (Cnd condition : andConditions) {
            if (condition == null) {
                continue;
            }
            Path<?> path = this.getPath(root, condition.property);
            if (path == null) {
                continue;
            }
            switch (condition.operator) {
                case eq:
                    if (condition.value != null) {
                        if (String.class.isAssignableFrom(path.getJavaType()) && condition.value instanceof String) {
                            if (!((String) condition.value).isEmpty()) {
                                restrictions = cb.and(restrictions, cb.equal(path, condition.value));
                            }
                        } else {
                            restrictions = cb.and(restrictions, cb.equal(path, condition.value));
                        }
                    }
                    break;
                case ge:
                    if (Number.class.isAssignableFrom(path.getJavaType()) && condition.value instanceof Number) {
                        restrictions = cb.and(restrictions, cb.ge((Path<Number>) path, (Number) condition.value));
                    }
                    break;
                case gt:
                    if (Number.class.isAssignableFrom(path.getJavaType()) && condition.value instanceof Number) {
                        restrictions = cb.and(restrictions, cb.gt((Path<Number>) path, (Number) condition.value));
                    }
                    break;
                case lt:
                    if (Number.class.isAssignableFrom(path.getJavaType()) && condition.value instanceof Number) {
                        restrictions = cb.and(restrictions, cb.lt((Path<Number>) path, (Number) condition.value));
                    }
                    break;
                case ne:
                    if (condition.value != null) {
                        if (String.class.isAssignableFrom(path.getJavaType()) && condition.value instanceof String && !((String) condition.value).isEmpty()) {
                            restrictions = cb.and(restrictions, cb.notEqual(path, condition.value));
                        } else {
                            restrictions = cb.and(restrictions, cb.notEqual(path, condition.value));
                        }
                    }
                    break;
                case isNotNull:
                    restrictions = cb.and(restrictions, path.isNotNull());
                    break;
            }
        }
        return restrictions;
    }
    private Predicate getOrPredicates(Root<T> root, CriteriaBuilder cb) {
        // 相同的逻辑 Need TODO
        return null;
    }
    private List<javax.persistence.criteria.Order> getOrders(Root<T> root, CriteriaBuilder cb) {
        List<javax.persistence.criteria.Order> orderList = new ArrayList<>();
        if (root == null || CollectionUtils.isEmpty(orders)) {
            return orderList;
        }
        for (Order order : orders) {
            if (order == null) {
                continue;
            }
            String property = order.getProperty();
            Sort.Direction direction = order.getDirection();
            Path<?> path = this.getPath(root, property);
            if (path == null || direction == null) {
                continue;
            }
            switch (direction) {
                case ASC:
                    orderList.add(cb.asc(path));
                    break;
                case DESC:
                    orderList.add(cb.desc(path));
                    break;
            }
        }
        return orderList;
    }
    /**
     * 获取Path
     *
     * @param path         Path
     * @param propertyPath 属性路径
     * @return Path
     */
    private <X> Path<X> getPath(Path<?> path, String propertyPath) {
        if (path == null || StringUtils.isEmpty(propertyPath)) {
            return (Path<X>) path;
        }
        String property = StringUtils.substringBefore(propertyPath, PROPERTY_SEPARATOR);
        return getPath(path.get(property), StringUtils.substringAfter(propertyPath, PROPERTY_SEPARATOR));
    }
    /**
     * 条件
     */
    public static class Cnd {
        Operator operator;
        String property;
        Object value;
        public Cnd(String property, Operator operator, Object value) {
            this.operator = operator;
            this.property = property;
            this.value = value;
        }
        /**
         * 相等
         *
         * @param property
         * @param value
         * @return
         */
        public static Cnd eq(String property, Object value) {
            return new Cnd(property, Operator.eq, value);
        }
        /**
         * 不相等
         *
         * @param property
         * @param value
         * @return
         */
        public static Cnd ne(String property, Object value) {
            return new Cnd(property, Operator.ne, value);
        }
    }
    /**
     * 排序
     */
    @Getter
    @Setter
    public static class Order {
        private String property;
        private Sort.Direction direction = Sort.Direction.ASC;
        /**
         * 构造方法
         *
         * @param property  属性
         * @param direction 方向
         */
        public Order(String property, Sort.Direction direction) {
            this.property = property;
            this.direction = direction;
        }
        /**
         * 返回递增排序
         *
         * @param property 属性
         * @return 递增排序
         */
        public static Order asc(String property) {
            return new Order(property, Sort.Direction.ASC);
        }
        /**
         * 返回递减排序
         *
         * @param property 属性
         * @return 递减排序
         */
        public static Order desc(String property) {
            return new Order(property, Sort.Direction.DESC);
        }
    }
    /**
     * 运算符
     */
    @Getter
    @Setter
    public enum Operator {
        /**
         * 等于
         */
        eq(" = "),
        /**
         * 不等于
         */
        ne(" != "),
        /**
         * 大于
         */
        gt(" > "),
        /**
         * 小于
         */
        lt(" < "),
        /**
         * 大于等于
         */
        ge(" >= "), 
        /**
         * 不为Null
         */
        isNotNull(" is not NULL ");
        Operator(String operator) {
            this.operator = operator;
        }
        private String operator;
    }
}


7、与之类似的解决方案还有 RSQL 的解决方案,可以参考Git_Hub上的此开源项目。

RSQL(RESTful Service Query Language)是 Feed Item Query Language (FIQL) 的超集,是一种 RESTful 服务的查询语言。这里我们使用 rsql-jpa 来实践,它依赖 rsql-parser 来解析 RSQL 语法,然后将解析后的 RSQL 转义到 JPA 的 Specification。

maven 的地址如下:

<dependency>
    <groupId>com.github.tennaito</groupId>
    <artifactId>rsql-jpa</artifactId>
    <version>2.0.2</version>
</dependency>

原文链接:Spring Data JPA动态SQL及自定义Repository

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值