در ادامه REST Query Language در این بخش به بررسی عملیات دیگری که در حین سرچ ممکن است نیاز داشته باشیم مانند Equality , Negation , Greater than , Less than , Starts with , Ends with , Contains و Like میپردازیم
در سه بخش قبلی با روش های JPA Criteria , Spring Data JPA Specification و QueryDSL آشنا شدیم از آنجا که روش Spring Data JPA Specification انعطاف و قابلیت بیشتری دارد و برای عملیات پیشرفته تر راه کار مناسب تری است در ادامه مبحث هم از همین روش استفاده خواهد شد
برای شروع از نظر طراحی بهتر است که عملیات را در قالب Enum تعریف کنیم و در طول برنامه از آن استفاده کنیم :
public enum SearchOperation { EQUALITY, NEGATION, GREATER_THAN, LESS_THAN, LIKE, STARTS_WITH, ENDS_WITH, CONTAINS; public static final String[] SIMPLE_OPERATION_SET = { ":", "!", ">", "<", "~" }; public static SearchOperation getSimpleOperation(char input) { switch (input) { case ':': return EQUALITY; case '!': return NEGATION; case '>': return GREATER_THAN; case '<': return LESS_THAN; case '~': return LIKE; default: return null; } } }
از علامت های بالا برای مشخص کردن نوع عملیات دریافتی از کلاینت استفاده میکنیم و سمت سرور آنها را به مقدار Enum مورد نظر تبدیل میکنیم
از کلاس SearchCriteria برای مشخص کردن نام فیلد، مقدار مورد نیاز در سرچ و عملیات مورد نظر استفاده میکنیم :
public class SearchCriteria { private String key; private SearchOperation operation; private Object value; }
کلاس UserSpecification برای تعیین مقادیر SearchCriteria ایجاد میکنیم:
public class UserSpecification implements Specification<User> { private SearchCriteria criteria; @Override public Predicate toPredicate( Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) { switch (criteria.getOperation()) { case EQUALITY: return builder.equal(root.get(criteria.getKey()), criteria.getValue()); case NEGATION: return builder.notEqual(root.get(criteria.getKey()), criteria.getValue()); case GREATER_THAN: return builder.greaterThan(root.<String> get( criteria.getKey()), criteria.getValue().toString()); case LESS_THAN: return builder.lessThan(root.<String> get( criteria.getKey()), criteria.getValue().toString()); case LIKE: return builder.like(root.<String> get( criteria.getKey()), criteria.getValue().toString()); case STARTS_WITH: return builder.like(root.<String> get(criteria.getKey()), criteria.getValue() + "%"); case ENDS_WITH: return builder.like(root.<String> get(criteria.getKey()), "%" + criteria.getValue()); case CONTAINS: return builder.like(root.<String> get( criteria.getKey()), "%" + criteria.getValue() + "%"); default: return null; } } }
تست Equality :
@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification( new SearchCriteria("firstName", SearchOperation.EQUALITY, "john")); UserSpecification spec1 = new UserSpecification( new SearchCriteria("lastName", SearchOperation.EQUALITY, "doe")); List<User> results = repository.findAll(Specification.where(spec).and(spec1)); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
تست Negation :
@Test public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification( new SearchCriteria("firstName", SearchOperation.NEGATION, "john")); List<User> results = repository.findAll(Specification.where(spec)); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }
تست Greater Than :
@Test public void givenMinAge_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification( new SearchCriteria("age", SearchOperation.GREATER_THAN, "25")); List<User> results = repository.findAll(Specification.where(spec)); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }
تست Starts With :
@Test public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification( new SearchCriteria("firstName", SearchOperation.STARTS_WITH, "jo")); List<User> results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
تست Ends With :
@Test public void givenFirstNameSuffix_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification( new SearchCriteria("firstName", SearchOperation.ENDS_WITH, "n")); List<User> results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
تست Contains :
@Test public void givenFirstNameSubstring_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification( new SearchCriteria("firstName", SearchOperation.CONTAINS, "oh")); List<User> results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
تست Range یا Between با کمک Greater Than و Less Than :
@Test public void givenAgeRange_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification( new SearchCriteria("age", SearchOperation.GREATER_THAN, "20")); UserSpecification spec1 = new UserSpecification( new SearchCriteria("age", SearchOperation.LESS_THAN, "25")); List<User> results = repository.findAll(Specification.where(spec).and(spec1)); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
در لایه وب برای اینکه عملیات ورودی کلاینت را مورد پردازش قرار دهیم نیاز است کلاس UserSpecificationsBuilder را آماده کنیم :
public class UserSpecificationsBuilder { private List<SearchCriteria> params; public UserSpecificationsBuilder with( String key, String operation, Object value, String prefix, String suffix) { SearchOperation op = SearchOperation.getSimpleOperation(operation.charAt(0)); if (op != null) { if (op == SearchOperation.EQUALITY) { boolean startWithAsterisk = prefix.contains("*"); boolean endWithAsterisk = suffix.contains("*"); if (startWithAsterisk && endWithAsterisk) { op = SearchOperation.CONTAINS; } else if (startWithAsterisk) { op = SearchOperation.ENDS_WITH; } else if (endWithAsterisk) { op = SearchOperation.STARTS_WITH; } } params.add(new SearchCriteria(key, op, value)); } return this; } public Specification<User> build() { if (params.size() == 0) { return null; } Specification result = new UserSpecification(params.get(0)); for (int i = 1; i < params.size(); i++) { result = params.get(i).isOrPredicate() ? Specification.where(result).or(new UserSpecification(params.get(i))) : Specification.where(result).and(new UserSpecification(params.get(i))); } return result; } }
طراحی کنترلر برای سرچ مقادیر و عملیات ارسالی از کلاینت :
@RequestMapping(method = RequestMethod.GET, value = "/users") @ResponseBody public List<User> findAllBySpecification(@RequestParam(value = "search") String search) { UserSpecificationsBuilder builder = new UserSpecificationsBuilder(); String operationSetExper = Joiner.on("|").join(SearchOperation.SIMPLE_OPERATION_SET); Pattern pattern = Pattern.compile( "(\\w+?)(" + operationSetExper + ")(\p{Punct}?)(\\w+?)(\p{Punct}?),"); Matcher matcher = pattern.matcher(search + ","); while (matcher.find()) { builder.with( matcher.group(1), matcher.group(2), matcher.group(4), matcher.group(3), matcher.group(5)); } Specification<User> spec = builder.build(); return dao.findAll(spec); }
کوئری :
http://localhost:8080/users?search=firstName:jo*,age<25
نمونه جواب کوئری سرچ کلاینت :
[{ "id":1, "firstName":"john", "lastName":"doe", "email":"john@doe.com", "age":24 }]
حالا نیاز است که API ساخته شده را تست کنیم
قبل از تست ابتدا چند Entity را ساخته و ذخیره میکنیم :
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration( classes = { ConfigTest.class, PersistenceConfig.class }, loader = AnnotationConfigContextLoader.class) @ActiveProfiles("test") public class JPASpecificationLiveTest { @Autowired private UserRepository repository; private User userJohn; private User userTom; private final String URL_PREFIX = "http://localhost:8080/users?search="; @Before public void init() { userJohn = new User(); userJohn.setFirstName("John"); userJohn.setLastName("Doe"); userJohn.setEmail("john@doe.com"); userJohn.setAge(22); repository.save(userJohn); userTom = new User(); userTom.setFirstName("Tom"); userTom.setLastName("Doe"); userTom.setEmail("tom@doe.com"); userTom.setAge(26); repository.save(userTom); } private RequestSpecification givenAuth() { return RestAssured.given().auth() .preemptive() .basic("username", "password"); } }
تست Equality :
@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { Response response = givenAuth().get(URL_PREFIX + "firstName:john,lastName:doe"); String result = response.body().asString(); assertTrue(result.contains(userJohn.getEmail())); assertFalse(result.contains(userTom.getEmail())); }
تست Negation :
@Test public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() { Response response = givenAuth().get(URL_PREFIX + "firstName!john"); String result = response.body().asString(); assertTrue(result.contains(userTom.getEmail())); assertFalse(result.contains(userJohn.getEmail())); }
تست Greater Than :
@Test public void givenMinAge_whenGettingListOfUsers_thenCorrect() { Response response = givenAuth().get(URL_PREFIX + "age>25"); String result = response.body().asString(); assertTrue(result.contains(userTom.getEmail())); assertFalse(result.contains(userJohn.getEmail())); }
تست Starts With :
@Test public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() { Response response = givenAuth().get(URL_PREFIX + "firstName:jo*"); String result = response.body().asString(); assertTrue(result.contains(userJohn.getEmail())); assertFalse(result.contains(userTom.getEmail())); }
تست Ends With :
@Test public void givenFirstNameSuffix_whenGettingListOfUsers_thenCorrect() { Response response = givenAuth().get(URL_PREFIX + "firstName:*n"); String result = response.body().asString(); assertTrue(result.contains(userJohn.getEmail())); assertFalse(result.contains(userTom.getEmail())); }
تست Contains :
@Test public void givenFirstNameSubstring_whenGettingListOfUsers_thenCorrect() { Response response = givenAuth().get(URL_PREFIX + "firstName:*oh*"); String result = response.body().asString(); assertTrue(result.contains(userJohn.getEmail())); assertFalse(result.contains(userTom.getEmail())); }
تست Range یا Between با ترکیب چند عملیات :
@Test public void givenAgeRange_whenGettingListOfUsers_thenCorrect() { Response response = givenAuth().get(URL_PREFIX + "age>20,age<25"); String result = response.body().asString(); assertTrue(result.contains(userJohn.getEmail())); assertFalse(result.contains(userTom.getEmail())); }