در این بخش خواهیم دید چطور میتوان بوسیله Spring Data JPA و QueryDSL کوئری های داینامیک روی دیتا ایجاد کرد و QueryDSL چه ویژگی ای را فراهم میکند
در ابتدا لازم است که کتابخانه مورد نیاز QueryDSL را به pom اضافه کنیم :
<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 Processing Tool انجام دهیم :
APT به عنوان یک پلاگین استفاده خواهد شد
<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>
Entity ای که میخواهیم search بر اساس فیلد های آن انجام شود :
@Entity public class MyUser { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age; }
ساخت Predicate سفارشی بوسیله PathBuilder :
برای داشتن Predicate منعطف تر نیاز است که حالت abstract تری داشته باشیم و از این رو از PathBuilder کمک میگیریم
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 قابل انجام هستند
Search Criteria :
از این کلاس برای نگهداری اطلاعات سرچ دریافتی از کلاینت استفاده میکنیم :
public class SearchCriteria { private String key; private String operation; private Object value; }
حالا نیاز به اینترفیسی داریم که عملیات سرچ را اجرا کند از این رو نیاز است علاوه بر ارث بری از JpaRepository از QuerydslPredicateExecutor هم ارث بری کند
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); } }
حالا میتوانیم با ترکیب کردن Predicate ها سرچ های پیچیده تری را پوشش دهیم
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 predicates = params.stream().map(param -> { MyUserPredicate predicate = new MyUserPredicate(param); return predicate.getPredicate(); }).filter(Objects::nonNull).collect(Collectors.toList()); BooleanExpression result = Expressions.asBoolean(true).isTrue(); for (BooleanExpression predicate : predicates) { result = result.and(predicate); } return result; } }
تست :
برای تست ابتدا دیتاهایی وارد دیتابیس میکنیم
@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); } }
تست سرچ بر اساس lastName :
@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)); }
تست بر اساس firstName و lastName :
@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))); }
تست سرچ بر اساس lastName و حداقل age :
@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()); }
سرچ بر اساس قسمتی از firstName :
@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))); }
ساخت و تست سرچ در مدل واقعی روی کنترلر و درخواست سرچ کلاینت :
@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); } }
درخواست سرچ کلاینت :
http://localhost:8080/myusers?search=lastName:doe,age>25
نمونه نتیجه ای که انتظار داریم ببینیم :
[{ "id":2, "firstName":"tom", "lastName":"doe", "email":"tom@doe.com", "age":26 }]