Автор оригинала: Eugen Paraschiv.
1. Обзор
В этой статье мы расширим Язык запросов REST, который мы разработали в предыдущих частях серии , чтобы включить больше операций поиска .
Теперь мы поддерживаем следующие операции: Равенство, Отрицание, Больше, Меньше, Начинается с, Заканчивается, Содержит и тому Подобное.
Обратите внимание, что мы рассмотрели три реализации – критерии JPA, спецификации Spring Data JPA и DSL запросов; мы продолжим работу со спецификациями в этой статье, потому что это чистый и гибкий способ представления наших операций.
2. Перечисление Операций Поиска
Во – первых, давайте начнем с определения лучшего представления наших различных поддерживаемых операций поиска – с помощью перечисления:
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; } } }
У нас есть два набора операций:
1. Простой – может быть представлен одним символом:
- Равенство: представлено двоеточием ( : )
- Отрицание: представлено восклицательным знаком ( ! )
- Больше, чем: представлено ( > )
- Меньше: представлено ( < )
- Например: представлен тильдой ( ~ )
2. Сложный – требуется более одного символа для представления:
- Начинается с: представлен ( =префикс* )
- Заканчивается: представлено ( =*суффикс )
- Содержит: представлено ( =*подстрока* )
Нам также необходимо изменить ваши Критерии поиска класс, чтобы использовать новую Операцию поиска :
public class SearchCriteria { private String key; private SearchOperation operation; private Object value; }
3. Измените Спецификацию Пользователя
Теперь – давайте включим новые поддерживаемые операции в нашу реализацию UserSpecification :
public class UserSpecification implements Specification{ private SearchCriteria criteria; @Override public Predicate toPredicate( Root 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. get( criteria.getKey()), criteria.getValue().toString()); case LESS_THAN: return builder.lessThan(root. get( criteria.getKey()), criteria.getValue().toString()); case LIKE: return builder.like(root. get( criteria.getKey()), criteria.getValue().toString()); case STARTS_WITH: return builder.like(root. get(criteria.getKey()), criteria.getValue() + "%"); case ENDS_WITH: return builder.like(root. get(criteria.getKey()), "%" + criteria.getValue()); case CONTAINS: return builder.like(root. get( criteria.getKey()), "%" + criteria.getValue() + "%"); default: return null; } } }
4. Тесты на стойкость
Далее – мы протестируем наши новые поисковые операции – на уровне постоянства:
4.1. Равенство тестов
В следующем примере – мы будем искать пользователя по его имени и фамилии :
@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")); Listresults = repository.findAll(Specification.where(spec).and(spec1)); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
4.2. Отрицание теста
Далее, давайте искать пользователей, которые по их имени не “джон” :
@Test public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification( new SearchCriteria("firstName", SearchOperation.NEGATION, "john")); Listresults = repository.findAll(Specification.where(spec)); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }
4.3. Тест Больше, Чем
Далее – мы будем искать пользователей с возрастом больше “25” :
@Test public void givenMinAge_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification( new SearchCriteria("age", SearchOperation.GREATER_THAN, "25")); Listresults = repository.findAll(Specification.where(spec)); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }
4.4. Тест Начинается С
Далее – пользователи с их именем, начинающимся с “jo” :
@Test public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification( new SearchCriteria("firstName", SearchOperation.STARTS_WITH, "jo")); Listresults = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
4.5. Тест Заканчивается
Далее мы будем искать пользователей с их именем, заканчивающимся на “n” :
@Test public void givenFirstNameSuffix_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification( new SearchCriteria("firstName", SearchOperation.ENDS_WITH, "n")); Listresults = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
4.6. Тест Содержит
Теперь мы будем искать пользователей с их именем, содержащим “oh” :
@Test public void givenFirstNameSubstring_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification( new SearchCriteria("firstName", SearchOperation.CONTAINS, "oh")); Listresults = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
4.7. Испытательный полигон
Наконец, мы будем искать пользователей с возрастом от “20” до “25” :
@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")); Listresults = repository.findAll(Specification.where(spec).and(spec1)); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
5. Конструктор Пользовательских Спецификаций
Теперь, когда настойчивость завершена и протестирована, давайте перенесем наше внимание на веб-уровень.
Мы будем строить поверх Построителя пользовательских спецификаций реализации из предыдущей статьи, чтобы включить новые новые операции поиска :
public class UserSpecificationsBuilder { private Listparams; 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 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; } }
6. Пользовательский контроллер
Далее – нам нужно изменить наш UserController , чтобы правильно анализировать новые операции :
@RequestMapping(method = RequestMethod.GET, value = "/users") @ResponseBody public ListfindAllBySpecification(@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 spec = builder.build(); return dao.findAll(spec); }
Теперь мы можем обратиться к API и получить правильные результаты с любой комбинацией критериев. Например, вот как будет выглядеть сложная операция с использованием API с языком запросов:
http://localhost:8080/users?search=firstName:jo*,age<25
И ответ:
[{ "id":1, "firstName":"john", "lastName":"doe", "email":"[email protected]", "age":24 }]
7. Тесты для API поиска
Наконец, давайте убедимся, что наш API работает хорошо, написав набор тестов API.
Мы начнем с простой настройки теста и инициализации данных:
@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("[email protected]"); userJohn.setAge(22); repository.save(userJohn); userTom = new User(); userTom.setFirstName("Tom"); userTom.setLastName("Doe"); userTom.setEmail("[email protected]"); userTom.setAge(26); repository.save(userTom); } private RequestSpecification givenAuth() { return RestAssured.given().auth() .preemptive() .basic("username", "password"); } }
7.1. Равенство тестов
Во – первых- давайте искать пользователя с именем “ джон “и фамилией” доу “ :
@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())); }
7.2. Отрицание теста
Теперь – мы будем искать пользователей, когда их имя не “джон” :
@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())); }
7.3. Тест Больше, Чем
Далее – мы будем искать пользователей с возрастом больше “25” :
@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())); }
7.4. Тест Начинается С
Далее – пользователи с их именем, начинающимся с “jo” :
@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())); }
7.5. Тест Заканчивается
Теперь – пользователи с их именем, заканчивающимся на “n” :
@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())); }
7.6. Тест Содержит
Далее мы будем искать пользователей с их именем, содержащим “oh” :
@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())); }
7.7. Испытательный полигон
Наконец, мы будем искать пользователей с возрастом от “20” до “25” :
@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())); }
8. Заключение
В этой статье мы перенесли язык запросов нашего REST Search API на зрелую, проверенную, производственную реализацию . Теперь мы поддерживаем широкий спектр операций и ограничений, что должно позволить довольно легко элегантно разрезать любой набор данных и получить точные ресурсы, которые мы ищем.
полную реализацию этой статьи можно найти в проекте GitHub – это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.