搜索和过滤:Spring Data JPA规格或规范模式实现 - Rahul Yadav


搜索和过滤是可以对数据集执行的最简单的操作之一。简而言之:过滤器类似于where典型数据库查询中的子句。

例如,考虑以下实体及其对应的dao存储库:

@Data
@Entity
public class User {

   @Id
   @GeneratedValue
   private Long id;

   private String name;

   @ManyToOne
   @JoinColumn(name = "account_id", updatable = false)
   private Account account;
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
   List<User> findByAccountId(Long accountId);
   List<User> findByName(String name);
   List<User> findByAccountIdAndName(Long accountId, String name);
}

正如我们所看到的,对于每个字段,只有一个查询用于对该特定字段进行过滤,并具有与所有其余字段的所有可能组合的查询。因此,在此实现中,例如,如果添加了新字段groupId,我们将不得不为所有可能的新组合添加大量查询。

幸运的是,这个O(n)问题可借助于Spring Data JPA 的Specification 接口,可以解决上述具有指数复杂性的问题。我们将看到如何将这个简单而有效的框架集成到我们的应用程序中。

可以将规格/规范(Specification )视为类似于where数据库查询的子句。第一部分包括创建规范接口的通用实现,该接口将能够处理通常用于过滤数据集的不同类型的操作。

@Slf4j
public class GenericSpecification<T> implements Specification<T> {
   
   private SearchCriteria searchCriteria;
   
   public GenericSpecification(final SearchCriteria searchCriteria){
      super();
      this.searchCriteria = searchCriteria;
   }
   
   @Override
   public Predicate toPredicate(Root<T> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
      List<Object> arguments = searchCriteria.getArguments();
      Object arg = arguments.get(0);

      switch (searchCriteria.getSearchOperation()) {
         case EQUALITY:
            return criteriaBuilder.equal(root.get(searchCriteria.getKey()), arg);
         case GREATER_THAN:
            return criteriaBuilder.greaterThan(root.get(searchCriteria.getKey()), (Comparable) arg);
         ...
         case IN:
   return root.get(searchCriteria.getKey()).in(arguments);
      }
   }
}
@Data
public class SearchCriteria {
  
   private String key;
   private SearchOperation searchOperation;
   private boolean isOrOperation;
   private List<Object> arguments;
}
public enum SearchOperation {
   
   EQUALITY, NEGATION, GREATER_THAN, LESS_THAN, LIKE, STARTS_WITH,
   ...
}

显而易见,该代码GenericSpecificationsBuilder基于Builder设计模式,该模式添加了规范,并在build函数中创建了一个统一的规范,将所有不同的标准组合到一个谓词中。
尽管我们可以直接使用上面的构建器,但是我也创建了一个工厂,该工厂包装了引擎盖支持的所有可能的搜索操作。

@Component
public class SpecificationFactory<T> {

    public Specification<T> isEqual(String key, Object arg) {
        GenericSpecificationsBuilder<T> builder = new GenericSpecificationsBuilder<>();
        return builder.with(key, SearchOperation.EQUALITY, Collections.singletonList(arg)).build();
    }
    ...
    public Specification<T> isGreaterThan(String key, Comparable arg) {
        GenericSpecificationsBuilder<T> builder = new GenericSpecificationsBuilder<>();
        return builder.with(key, SearchOperation.GREATER_THAN, Collections.singletonList(arg)).build();
    }
}

现在,如果我们需要基于新条件添加过滤器,我们要做的就是将规范条件包装在另一个if语句中。

缺点:
使用上述实现的缺点之一是我们不再具有类型安全性。例如,如果发生以下情况,将抛出运行时异常:

  • 参数不匹配:例如,将类型的字段int传递一个String值作为参数。
  • 找不到字段:规范基于实体中不存在的字段。

结论
搜索和过滤问题对于所有现代应用程序都是微不足道的,并且Spring Data JPA规范提供了一种简洁而优雅的方法来创建动态查询。请分享您对如何解决搜索和过滤问题的想法和建议。