Spring Data Jpa 使用注解实现自定义查询

前言

最近看了一个 spring boot 的开源项目,项目地址为https://github.com/elunez/eladmin-web,里面有些思想很值得学习。比如它使用了注解,来快速的生成查询条件,使用起来非常方便,可以在项目开发中引用,这篇文章就介绍了注解的使用方法。

使用示例

下面列举一个简单的例子,不涉及到联表查询。

首先定义一个包含查询字段的类,然后使用注解来指定查询方式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Data
public class BookQueryCriteria {

    // 等于
    @Query
    private String author;

    // 小于等于
    @Query(type = Query.Type.GREATER_THAN, propName = "publishDate")
    private Date startDate;

   // IN 查询
    @Query(type = Query.Type.IN)
    private List<String> titles;
}

创建 repository,需要继承 JpaSpecificationExecutor才能支持动态查询

1
2
3
public interface BookRespository extends JpaRepository<Book, Integer>, JpaSpecificationExecutor<Book> {
    
}

使用 repository 查询

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
QueryCriteria criteria = new QueryCriteria()
criteria.setAuthor("作家A");
criteria.setStartTime("2020-01-01");
List<String> titles = new ArrayList<>();
titles.add("titleA");
titles.add("titleB")
criteria.setTitles(titles);

repositry.findAll(new Specification() { 
    @Override
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        return QueryHelp.getPredicate(root,criteria,criteriaBuilder);   
    }
});

如果支持 lambda 函数,代码可以进一步简化。因为Specification是一个接口,只需要实现toPredicate方法。

1
2
repositry.findAll((root, criteriaQuery, criteriaBuilder) -> 
                  QueryHelp.getPredicate(root,criteria,criteriaBuilder));

Query 注解

在了解初步使用后,再来看看它是如何实现的。首先从 Query 注解来开始,它定义了查询方式和查询字段。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Query {
    
    // 查询字段,默认为同名字段
    String propName() default "";
    
    // 查询方式,有 = , <=, >=, like, in 等查询方式
    Type type() default Type.EQUAL;
    
}

下面还定义了查询方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    enum Type {
        NOT_NULL            // 不为空  
        EQUAL,              // 等于
        NOT_EQUAL,          // 不等于
        GREATER_THAN,       // 大于等于
        LESS_THAN,          // 小于等于
        LESS_THAN_NQ        // 小于
        IN,                 // 包含
        BETWEEN           // between        
        INNER_LIKE,         // 中模糊查询
        LEFT_LIKE,          // 左模糊查询,后缀匹配
        RIGHT_LIKE,         // 右模糊查询,前缀匹配
    }

注解生效

在定义好 Query 注解后, eladmin通过QueryHelp,遍历字段的注解来生成 Predicate实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class QueryHelp {
    public static <R, Q> Predicate getPredicate(Root<R> root, Q query, CriteriaBuilder cb) {
        // 查询条件列表
        List<Predicate> list = new ArrayList<>();
        // 获取类的字段列表
        List<Field> fields = getAllFields(query.getClass(), new ArrayList<>());
        for (Field field : fields) {
            boolean accessible = field.isAccessible();
            field.setAccessible(true);
            // 获取Query注解
            Query q = field.getAnnotation(Query.class);
            if (q != null) {
                // 获取查询字段,如果没有指定propName,则使用同名
                String propName = q.propName();
                String attributeName = isBlank(propName) ? field.getName() : propName;
                // 获取查询值
                Object val = field.get(query);
                // 获取字段类型
                Class<?> fieldType = field.getType();
                switch (q.type()) {
                    case EQUAL:
                        Expression<fieldType> expression = root.get(attributeName).as((Class<? extends Comparable>) fieldType);
                        Predicate predicate = cb.equal(expression, val);
                        list.add(predicate);
                        break;
                    case GREATER_THAN:
                        //.....
                }
            }
            field.setAccessible(accessible);
        }
        int size = list.size();
        // 使用and组合查询条件,表示查询条件列表都要满足
        return cb.and(list.toArray(new Predicate[size]));
    }
}        

上面的代码只是展示了 EQUAL查询条件的处理,别的条件处理原理都相同。eladmin使用反射来遍历字段,获取它们的注解。根据注解不同,生成对应的Predicate实例,最后使用 and将这些条件组合在一起。

updatedupdated2023-07-022023-07-02