案例概述
在本教程中,咱們將研究使用Spring Data JPA和Querydsl爲REST API構建查詢語言。java
在本系列的前兩篇文章中,咱們使用JPA Criteria和Spring Data JPA規範構建了相同的搜索/過濾功能。數據庫
那麼 - 爲何要使用查詢語言?由於 - 對於任何複雜的API來講 - 經過很是簡單的字段搜索/過濾資源是不夠的。查詢語言更靈活,容許您精確過濾所需的資源。json
Querydsl配置
首先 - 讓咱們看看如何配置咱們的項目以使用Querydsl。bash
咱們須要將如下依賴項添加到pom.xml:app
<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處理工具 - 插件以下:less
<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」實體:maven
@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。ide
咱們在這裏使用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庫。