当前位置: 首页 > news >正文

Spring Data Specifications入门教程

Spring Data Specifications入门教程

如果您正在寻找一种更好的方法来管理您的查询或想要生成动态和类型安全的查询,那么您可能会在 Spring Data JPA Specifications中找到您的解决方案。

代码示例

本文附有GitHub上的工作代码示例。

什么是Specifications?

Spring Data JPA Specifications 是我们可以使用的另一个工具,可以使用 Spring 或 Spring Boot 执行数据库查询。

Specifications 建立在 Criteria API 之上。

当构建条件查询时,我们需要自己建立和管理RootCriteraQuery以及CriteriaBuilder对象:

...
EntityManager entityManagr = getEntityManager();

CriteriaBuilder builder = entityManager.getCriteriaBuilder();

CriteriaQuery<Product> productQuery = builder.createQuery(Product.class);

Root<Person> personRoot = productQuery.from(Product.class);
...

Specification建立在 Criteria API 之上,以简化开发人员体验。我们只需要实现Specification接口:

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

}

使用规范我们可以构建原子谓词(atomic predicates),并结合这些谓词来构建复杂的动态查询。

Specifications 的灵感来自领域驱动设计“规范”模式。

为什么我们需要Specifications?

在 Spring Boot 中执行查询的最常见方法之一是使用如下查询方法:

interface ProductRepository extends JpaRepository<Product, String>, 
                  JpaSpecificationExecutor<Product> {
  
  List<Product> findAllByNameLike(String name);
  
  List<Product> findAllByNameLikeAndPriceLessThanEqual(
                  String name, 
                  Double price
                  );
  
  List<Product> findAllByCategoryInAndPriceLessThanEqual(
                  List<Category> categories, 
                  Double price
                  );
  
  List<Product> findAllByCategoryInAndPriceBetween(
                  List<Category> categories,
                  Double bottom, 
                  Double top
                  );
  
  List<Product> findAllByNameLikeAndCategoryIn(
                  String name, 
                  List<Category> categories
                  );
  
  List<Product> findAllByNameLikeAndCategoryInAndPriceBetween(
                  String name, 
                  List<Category> categories,
                  Double bottom, 
                  Double top
                  );
}

查询方法(query methods)的问题是我们只能指定固定数量的标准。此外,查询方法的数量随着用例的增加而迅速增加。

在某些时候,查询方法中有许多重叠的条件,如果其中任何一种发生变化,我们将不得不对多种查询方法进行更改。

此外,当我们的查询中有很长的字段名称和多个条件时,查询方法的长度可能会显着增加。因此,可能需要一段时间才能理解如此冗长的查询及其目的:

List<Product> findAllByNameLikeAndCategoryInAndPriceBetweenAndManufacturingPlace_State(String name,
                                             List<Category> categories,
                                             Double bottom, Double top,
                                             STATE state);

使用Specifications,我们可以通过创建原子谓词来解决这些问题。通过给这些谓词一个有意义的名字,我们可以清楚地说明它们的意图。我们将在使用规范编写查询部分中了解如何将上述内容转换为更有意义的查询 。

Specifications允许我们以编程方式编写查询。因此,我们可以根据用户输入动态构建查询。我们将在下面的利用Specifications进行动态查询部分中更详细地了解这一点。

初始设置

首先,我们需要在我们的build.gradle文件中添加 Spring Data Jpa 依赖:

...
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
annotationProcessor 'org.hibernate:hibernate-jpamodelgen'
...

我们还添加了hibernate-jpamodelgen注释处理器依赖项,它将生成我们实体的静态元模型类。

生成的元模型

Hibernate JPA 模型生成器生成的类将允许我们以强类型方式编写查询。

例如,让我们看看 JPA 实体Distributor:

@Entity
public class Distributor {
  @Id
  private String id;

  private String name;

  @OneToOne
  private Address address;
  //Getter setter ignored for brevity 

}

Distributor实体的元模型类如下所示:

@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Distributor.class)
public abstract class Distributor_ {

  public static volatile SingularAttribute<Distributor, Address> address;
  public static volatile SingularAttribute<Distributor, String> name;
  public static volatile SingularAttribute<Distributor, String> id;
  public static final String ADDRESS = "address";
  public static final String NAME = "name";
  public static final String ID = "id";

}

我们现在可以在标准查询中使用Distributor_.name,而不是直接使用实体的字符串字段名称。这样做的一个主要好处是使用元模型的查询随实体一起变化,并且比字符串查询更容易重构。

使用Specifications编写查询

让我们将上面提到的查询findAllByNameLike()转换为一个Specification:

List<Product> findAllByNameLike(String name);

该查询方法的等效于Specification:

private Specification<Product> nameLike(String name){
  return new Specification<Product>() {
   @Override
   public Predicate toPredicate(Root<Product> root, 
                  CriteriaQuery<?> query, 
                  CriteriaBuilder criteriaBuilder) {
     return criteriaBuilder.like(root.get(Product_.NAME), "%"+name+"%");
   }
  };
}

使用 Java 8 Lambda,我们可以将上述内容简化为以下内容:

private Specification<Product> nameLike(String name){
  return (root, query, criteriaBuilder) 
      -> criteriaBuilder.like(root.get(Product_.NAME), "%"+name+"%");
}

我们也可以在代码中需要它的地方内联编写它:

...
Specification<Product> nameLike = 
      (root, query, criteriaBuilder) -> 
         criteriaBuilder.like(root.get(Product_.NAME), "%"+name+"%");
...

但这违背了我们可重用性的目的,所以除非我们的用例需要,否则让我们避免这种情况。

要执行Specifications,我们需要扩展Spring Data JPA 存储库中的接口JpaSpecificationExecutor

interface ProductRepository extends JpaRepository<Product, String>, 
                  JpaSpecificationExecutor<Product> {
}

该JpaSpecificationExecutor接口添加了允许我们执行Specifications 的方法,例如,这些:

List<T> findAll(Specification<T> spec);

Page<T> findAll(Specification<T> spec, Pageable pageable);

List<T> findAll(Specification<T> spec, Sort sort);

最后,要执行我们的查询,我们可以简单地调用:

List<Product> products = productRepository.findAll(namelike("reflectoring"));

我们还可以利用带有Pageable和Sort参数的重载方法findAll(),在这种情况下,我们可以对结果分页或者排序。

该Specification接口还具有公共静态辅助方法and()、or()和where(),这允许我们组合多个Specification。它还提供了not(),允许我们否定一个Specification。

让我们看一个例子:

public List<Product> getPremiumProducts(String name, 
                    List<Category> categories) {
  return productRepository.findAll(
      where(belongsToCategory(categories))
          .and(nameLike(name))
          .and(isPremium()));
}

private Specification<Product> belongsToCategory(List<Category> categories){
  return (root, query, criteriaBuilder)-> 
      criteriaBuilder.in(root.get(Product_.CATEGORY)).value(categories);
}

private Specification<Product> isPremium() {
  return (root, query, criteriaBuilder) ->
      criteriaBuilder.and(
          criteriaBuilder.equal(
              root.get(Product_.MANUFACTURING_PLACE)
                        .get(Address_.STATE),
              STATE.CALIFORNIA),
          criteriaBuilder.greaterThanOrEqualTo(
              root.get(Product_.PRICE), PREMIUM_PRICE));
}

在这里,我们使用where()and()辅助函数将belongsToCategory()、nameLike()和isPremium()规范(specifications)合二为一。这也很好读,你不觉得吗?另外,请注意isPremium()如何赋予查询更多意义。

目前,isPremium()正在组合两个谓词,但如果我们愿意,我们可以为每个谓词创建单独的规范,然后再次通过and()来组合。现在,我们将保持原样,因为 在isPremium()中使用的谓词在该查询中是特定的,如果将来我们也需要在其他查询中使用它们,那么我们总是可以在不影响isPremium()函数的使用者的情况下拆分它们。

利用Specifications进行动态查询

假设我们想要创建一个 API,允许我们的客户获取所有产品并根据许多属性(例如类别、价格、颜色等)过滤它们。在这里,我们事先不知道哪些属性组合客户将用于过滤产品。

处理此问题的一种方法是为所有可能的组合编写查询方法,但这需要编写大量查询方法。随着我们引入新领域,这个数字会组合增加。

更好的解决方案是直接从客户端获取谓词,并使用规范将它们转换为数据库查询语句。客户端只需向我们提供Filters的列表,我们的后端将负责其余的工作。让我们看看如何做到这一点。

首先,让我们创建一个输入对象来从客户端获取过滤器:

public class Filter {
  private String field;
  private QueryOperator operator;
  private String value;
  private List<String> values;//Used in case of IN operator
}

我们将通过 REST API 将此对象公开给我们的客户。

其次,我们需要编写方法可以将Filter转成Specification:

private Specification<Product> createSpecification(Filter input) {
  switch (input.getOperator()){
    
    case EQUALS:
       return (root, query, criteriaBuilder) -> 
          criteriaBuilder.equal(root.get(input.getField()),
           castToRequiredType(root.get(input.getField()).getJavaType(), 
                              input.getValue()));
    
    case NOT_EQUALS:
       return (root, query, criteriaBuilder) -> 
          criteriaBuilder.notEqual(root.get(input.getField()),
           castToRequiredType(root.get(input.getField()).getJavaType(), 
                              input.getValue()));
    
    case GREATER_THAN:
       return (root, query, criteriaBuilder) -> 
          criteriaBuilder.gt(root.get(input.getField()),
           (Number) castToRequiredType(
                  root.get(input.getField()).getJavaType(), 
                              input.getValue()));
    
    case LESS_THAN:
       return (root, query, criteriaBuilder) -> 
          criteriaBuilder.lt(root.get(input.getField()),
           (Number) castToRequiredType(
                  root.get(input.getField()).getJavaType(), 
                              input.getValue()));
    
    case LIKE:
      return (root, query, criteriaBuilder) -> 
          criteriaBuilder.like(root.get(input.getField()), 
                          "%"+input.getValue()+"%");
    
    case IN:
      return (root, query, criteriaBuilder) -> 
          criteriaBuilder.in(root.get(input.getField()))
          .value(castToRequiredType(
                  root.get(input.getField()).getJavaType(), 
                  input.getValues()));
    
    default:
      throw new RuntimeException("Operation not supported yet");
  }
}

在这里,我们支持多种操作,如EQUALS,LESS_THAN,IN等,我们也可以根据自己的需求添加更多。

现在,正如我们所知,Criteria API 允许我们编写类型安全的查询。因此,我们提供的值的类型必须与我们的字段类型兼容。Filter将值作为String这意味着我们必须将值转换为所需的类型,然后再将其传递给CriteriaBuilder:

private Object castToRequiredType(Class fieldType, String value) {
  if(fieldType.isAssignableFrom(Double.class)) {
    return Double.valueOf(value);
  } else if(fieldType.isAssignableFrom(Integer.class)) {
    return Integer.valueOf(value);
  } else if(Enum.class.isAssignableFrom(fieldType)) {
    return Enum.valueOf(fieldType, value);
  }
  return null;
}

private Object castToRequiredType(Class fieldType, List<String> value) {
  List<Object> lists = new ArrayList<>();
  for (String s : value) {
    lists.add(castToRequiredType(fieldType, s));
  }
  return lists;
}

最后,我们添加一个函数,将多个过滤器组合到一个规范中:

private Specification<Product> getSpecificationFromFilters(List<Filter> filter){
  Specification<Product> specification = 
            where(createSpecification(queryInput.remove(0)));
  for (Filter input : filter) {
    specification = specification.and(createSpecification(input));
  }
  return specification;
}

现在,让我们尝试使用我们新的动态specifications查询生成器获取属于MOBILE或TV APPLIANCE类别且价格低于 1000的所有产品。

Filter categories = Filter.builder()
     .field("category")
     .operator(QueryOperator.IN)
     .values(List.of(Category.MOBILE.name(), 
             Category.TV_APPLIANCES.name()))
     .build();

Filter lowRange = Filter.builder()
    .field("price")
    .operator(QueryOperator.LESS_THAN)
    .value("1000")
    .build();

List<Filter> filters = new ArrayList<>();
filters.add(lowRange);
filters.add(categories);

productRepository.getQueryResult(filters);

上面的代码片段应该适用于大多数过滤器情况,但仍有很大的改进空间。例如允许基于嵌套实体属性​​ ( manufacturingPlace.state) 的查询或限制我们希望允许过滤器的字段。将此视为一个开放式问题。

什么时候应该使用规范而不是查询方法?

想到的一个问题是,如果我们可以编写任何带有规范的查询,那么我们什么时候更喜欢查询方法?或者我们应该更喜欢它们吗?我相信在某些情况下查询方法可以派上用场。

假设我们的实体只有少数几个字段,它只需要以某种方式查询,那么当我们可以简单地编写查询方法时,为什么还要编写规范呢?

如果未来需要对给定实体进行更多查询,那么我们总是可以重构它以使用规范。此外,如果我们想在查询中使用特定于数据库的功能,例如使用 PostgresSQL 执行 JSON 查询,则规范将无济于事。

结论

规范为我们提供了一种编写可重用查询和流畅 API 的方法,我们可以使用这些 API 组合和构建更复杂的查询。

总而言之,无论我们想要创建可重用谓词还是想要以编程方式生成类型安全查询,Spring JPA 规范都是一个很好的工具。

感谢您的阅读!您可以在GitHub上找到工作代码。


来自:
Getting Started with Spring Data Specifications - Reflectoring

相关文章:

  • Job for docker.service failed because the control process exited with error
  • 在虚拟机配置docker redis环境
  • JPA CrudRepository方法详解
  • PowerDesigner 16.5 name和code自动同步问题
  • ShiroConfig开启Shiro的注解
  • webstorm tab缩进2空格还是4空格?
  • el-select 字符串多选回显
  • 字符串列表转成一个字符串 java
  • el-upload 第二次点击修改后文件跳动问题
  • ElementUI el-table 表格 行选择框改为单选
  • el-table handleCurrentChange有时候会失效
  • el-upload删除文件后修改文件仍然存在
  • element upload上传从上到下滑动 去除upload组件过渡效果
  • 正则表达式 匹配一个数字
  • js中的filter方法和map方法
  • [分享]iOS开发 - 实现UITableView Plain SectionView和table不停留一起滑动
  • Apache的基本使用
  • Docker: 容器互访的三种方式
  • Facebook AccountKit 接入的坑点
  • Laravel Telescope:优雅的应用调试工具
  • Mac 鼠须管 Rime 输入法 安装五笔输入法 教程
  • Python学习之路16-使用API
  • tensorflow学习笔记3——MNIST应用篇
  • 开源地图数据可视化库——mapnik
  • 一道面试题引发的“血案”
  • 从如何停掉 Promise 链说起
  • 如何正确理解,内页权重高于首页?
  • ​第20课 在Android Native开发中加入新的C++类
  • ​你们这样子,耽误我的工作进度怎么办?
  • #单片机(TB6600驱动42步进电机)
  • (1)(1.13) SiK无线电高级配置(五)
  • (1)bark-ml
  • (附源码)springboot高校宿舍交电费系统 毕业设计031552
  • (六)库存超卖案例实战——使用mysql分布式锁解决“超卖”问题
  • (七)MySQL是如何将LRU链表的使用性能优化到极致的?
  • (强烈推荐)移动端音视频从零到上手(下)
  • (十) 初识 Docker file
  • (太强大了) - Linux 性能监控、测试、优化工具
  • (学习日记)2024.04.10:UCOSIII第三十八节:事件实验
  • (转)es进行聚合操作时提示Fielddata is disabled on text fields by default
  • .NET 4.0中使用内存映射文件实现进程通讯
  • .net 反编译_.net反编译的相关问题
  • .NET 同步与异步 之 原子操作和自旋锁(Interlocked、SpinLock)(九)
  • .NET项目中存在多个web.config文件时的加载顺序
  • [ C++ ] STL_list 使用及其模拟实现
  • [ 隧道技术 ] cpolar 工具详解之将内网端口映射到公网
  • [1127]图形打印 sdutOJ
  • [BZOJ] 2006: [NOI2010]超级钢琴
  • [CF]Codeforces Round #551 (Div. 2)
  • [Java][Liferay] File system in liferay
  • [NOIP2007 普及组] 纪念品分组--贪心算法
  • [OGRE]看备注学编程(02):打地鼠01-布置场地九只地鼠
  • [poj 3461]Oulipo[kmp]
  • [Real world Haskell] 中文翻译:第二章 类型与函数
  • [UE4]创建自定义AIController的方法(C++)