案例概述
在本教程中,我们将研究使用Spring Data JPA和Querydsl为REST API构建查询语言。
在本系列的前两篇文章中,我们使用JPA Criteria和Spring Data JPA规范构建了相同的搜索/过滤功能。
那么 - 为什么要使用查询语言 ?因为 - 对于任何复杂的API来说 - 通过非常简单的字段搜索/过滤资源是不够的。查询语言更灵活,允许您精确过滤所需的资源。
Querydsl配置
首先 - 让我们看看如何配置我们的项目以使用Querydsl。
我们需要将以下依赖项添加到pom.xml:
<dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>4.1.4</version> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>4.1.4</version> </dependency>
我们还需要配置APT - Annotation处理工具 - 插件如下:
<plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin>
MyUser Entity
接下来 - 让我们看一下我们将在Search API中使用的“MyUser”实体:
@Entity public class MyUser { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age; }
使用PathBuilder自定义Predicate
现在 - 让我们根据一些任意约束创建一个自定义Predicate。
我们在这里使用PathBuilder而不是自动生成的Q类型,因为我们需要动态创建路径以获得更抽象的用法:
public class MyUserPredicate { private SearchCriteria criteria; public BooleanExpression getPredicate() { PathBuilder<MyUser> entityPath = new PathBuilder<>(MyUser.class, "user"); if (isNumeric(criteria.getValue().toString())) { NumberPath<Integer> path = entityPath.getNumber(criteria.getKey(), Integer.class); int value = Integer.parseInt(criteria.getValue().toString()); switch (criteria.getOperation()) { case ":": return path.eq(value); case ">": return path.goe(value); case "<": return path.loe(value); } } else { StringPath path = entityPath.getString(criteria.getKey()); if (criteria.getOperation().equalsIgnoreCase(":")) { return path.containsIgnoreCase(criteria.getValue().toString()); } } return null; } }
请注意Predicate的实现是通常如何处理多种类型的操作。这是因为查询语言根据定义是一种开放式语言,您可以使用任何支持的操作对任何字段进行过滤。
为了表示这种开放式过滤标准,我们使用了一个简单但非常灵活的实现 - SearchCriteria:
public class SearchCriteria { private String key; private String operation; private Object value; }
MyUserRepository
现在 - 让我们来看看我们的MyUserRepository。
我们需要MyUserRepository来扩展QueryDslPredicateExecutor,以便我们以后可以使用Predicates来过滤搜索结果:
public interface MyUserRepository extends JpaRepository<MyUser, Long>, QueryDslPredicateExecutor<MyUser>, QuerydslBinderCustomizer<QMyUser> { @Override default public void customize( QuerydslBindings bindings, QMyUser root) { bindings.bind(String.class) .first((SingleValueBinding<StringPath, String>) StringExpression::containsIgnoreCase); bindings.excluding(root.email); } }
结合Predicates
接下来让我们看看组合Predicates在结果过滤中使用多个约束。
在以下示例中 - 我们使用构建器 - MyUserPredicatesBuilder - 来组合Predicates:
public class MyUserPredicatesBuilder { private List<SearchCriteria> params; public MyUserPredicatesBuilder() { params = new ArrayList<>(); } public MyUserPredicatesBuilder with( String key, String operation, Object value) { params.add(new SearchCriteria(key, operation, value)); return this; } public BooleanExpression build() { if (params.size() == 0) { return null; } List<BooleanExpression> predicates = new ArrayList<>(); MyUserPredicate predicate; for (SearchCriteria param : params) { predicate = new MyUserPredicate(param); BooleanExpression exp = predicate.getPredicate(); if (exp != null) { predicates.add(exp); } } BooleanExpression result = predicates.get(0); for (int i = 1; i < predicates.size(); i++) { result = result.and(predicates.get(i)); } return result; } }
测试搜索查询
接下来 - 让我们测试一下我们的Search API。
我们将首先使用少数用户初始化数据库 - 准备好这些数据并进行测试:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { PersistenceConfig.class }) @Transactional @Rollback public class JPAQuerydslIntegrationTest { @Autowired private MyUserRepository repo; private MyUser userJohn; private MyUser userTom; @Before public void init() { userJohn = new MyUser(); userJohn.setFirstName("John"); userJohn.setLastName("Doe"); userJohn.setEmail("john@doe.com"); userJohn.setAge(22); repo.save(userJohn); userTom = new MyUser(); userTom.setFirstName("Tom"); userTom.setLastName("Doe"); userTom.setEmail("tom@doe.com"); userTom.setAge(26); repo.save(userTom); } }
接下来,让我们看看如何查找具有 给定姓氏的用户 :
@Test public void givenLast_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("lastName", ":", "Doe"); Iterable<MyUser> results = repo.findAll(builder.build()); assertThat(results, containsInAnyOrder(userJohn, userTom)); }
现在,让我们看看如何找到 具有名字和姓氏的用户 :
@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder() .with("firstName", ":", "John").with("lastName", ":", "Doe"); Iterable<MyUser> results = repo.findAll(builder.build()); assertThat(results, contains(userJohn)); assertThat(results, not(contains(userTom))); }
接下来,让我们看看如何找到 具有姓氏和最小年龄的用户
@Test public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder() .with("lastName", ":", "Doe").with("age", ">", "25"); Iterable<MyUser> results = repo.findAll(builder.build()); assertThat(results, contains(userTom)); assertThat(results, not(contains(userJohn))); }
接下来,让我们搜索 实际不存在 的用户:
@Test public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder() .with("firstName", ":", "Adam").with("lastName", ":", "Fox"); Iterable<MyUser> results = repo.findAll(builder.build()); assertThat(results, emptyIterable()); }
最后 - 让我们看看如何找到 仅给出名字的一部分的MyUser - 如下例所示:
@Test public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("firstName", ":", "jo"); Iterable<MyUser> results = repo.findAll(builder.build()); assertThat(results, contains(userJohn)); assertThat(results, not(contains(userTom))); }
UserController
最后,让我们将所有内容放在一起并构建REST API。
我们定义了一个UserController,它定义了一个带有“search”参数的简单方法findAll()来传递查询字符串:
@Controller public class UserController { @Autowired private MyUserRepository myUserRepository; @RequestMapping(method = RequestMethod.GET, value = "/myusers") @ResponseBody public Iterable<MyUser> search(@RequestParam(value = "search") String search) { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder(); if (search != null) { Pattern pattern = Pattern.compile("(/w+?)(:|<|>)(/w+?),"); Matcher matcher = pattern.matcher(search + ","); while (matcher.find()) { builder.with(matcher.group(1), matcher.group(2), matcher.group(3)); } } BooleanExpression exp = builder.build(); return myUserRepository.findAll(exp); } }
这是一个快速测试URL示例:
http://localhost:8080/myusers?search=lastName:doe,age>25
回应:
[{ "id":2, "firstName":"tom", "lastName":"doe", "email":"tom@doe.com", "age":26 }]
案例结论
第三篇文章介绍了为REST API构建查询语言的第一步,充分利用了Querydsl库。