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