در این بخش به کمک Spring Data JPA به ایجاد Search / Filtering REST Api می پردازیم و خواهیم دید چطوری با JPA Criteria کوئری مورد نظر را بسازیم
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age; // standard getters and setters }
در کلاس UserSpecification که اینترفیس Specification را پیاده سازی کرده است کوئری مورد نظر را با استفاده از SearchCreteria میسازیم :
public class UserSpecification implements Specification<User> { private SearchCriteria criteria; @Override public Predicate toPredicate (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) { if (criteria.getOperation().equalsIgnoreCase(">")) { return builder.greaterThanOrEqualTo( root.<String> get(criteria.getKey()), criteria.getValue().toString()); } else if (criteria.getOperation().equalsIgnoreCase("<")) { return builder.lessThanOrEqualTo( root.<String> get(criteria.getKey()), criteria.getValue().toString()); } else if (criteria.getOperation().equalsIgnoreCase(":")) { if (root.get(criteria.getKey()).getJavaType() == String.class) { return builder.like( root.<String>get(criteria.getKey()), "%" + criteria.getValue() + "%"); } else { return builder.equal(root.get(criteria.getKey()), criteria.getValue()); } } return null; } }
کلاس SearchCreteria که نام فیلد و نوع عملیات و مقداری که در حین عملیات نیاز است را نگهداری میکند
public class SearchCriteria { private String key; private String operation; private Object value; }
ساخت JPARespository به عنوان Bean برای کار با دیتابیس :
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {}
قبل از تست اصلی یکسری Entity در دیتابیس ذخیره میکنیم :
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { PersistenceJPAConfig.class }) @Transactional @TransactionConfiguration public class JPASpecificationsTest { @Autowired private UserRepository repository; private User userJohn; private User userTom; @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); } }
تست دریافت Entity ها بر اساس کوئری روی lastName :
@Test public void givenLast_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification(new SearchCriteria("lastName", ":", "doe")); List<User> results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, isIn(results)); }
تست کوئری بر اساس lastName و firstName :
@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { UserSpecification spec1 = new UserSpecification(new SearchCriteria("firstName", ":", "john")); UserSpecification spec2 = new UserSpecification(new SearchCriteria("lastName", ":", "doe")); List<User> results = repository.findAll(Specification.where(spec1).and(spec2)); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
دقت کنید که در کد بالا از عبارات شرطی where و and استفاده شده
تست کوئری بر اساس lastName , firstName و age :
@Test public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() { UserSpecification spec1 = new UserSpecification(new SearchCriteria("age", ">", "25")); UserSpecification spec2 = new UserSpecification(new SearchCriteria("lastName", ":", "doe")); List<User> results = repository.findAll(Specification.where(spec1).and(spec2)); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }
تست کوئری ای که هیچ مقداری را پیدا نخواهد کرد :
@Test public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { UserSpecification spec1 = new UserSpecification(new SearchCriteria("firstName", ":", "Adam")); UserSpecification spec2 = new UserSpecification(new SearchCriteria("lastName", ":", "Fox")); List<User> results = repository.findAll(Specification.where(spec1).and(spec2)); assertThat(userJohn, not(isIn(results))); assertThat(userTom, not(isIn(results))); }
تست کوئری بر اساس قسمتی از firstName :
@Test public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification(new SearchCriteria("firstName", ":", "jo")); List<User> results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
تست کوئری پیچیده تر و ترکیبی :
public class UserSpecificationsBuilder { private final List<SearchCriteria> params; public UserSpecificationsBuilder() { params = new ArrayList<SearchCriteria>(); } public UserSpecificationsBuilder with(String key, String operation, Object value) { params.add(new SearchCriteria(key, operation, value)); return this; } public Specification<User> build() { if (params.size() == 0) { return null; } List<Specification> specs = params.stream() .map(UserSpecification::new) .collect(Collectors.toList()); Specification result = specs.get(0); for (int i = 1; i < params.size(); i++) { result = params.get(i) .isOrPredicate() ? Specification.where(result) .or(specs.get(i)) : Specification.where(result) .and(specs.get(i)); } return result; } }
کوئری دریافتی از کلاینت روی کنترلر :
@Controller public class UserController { @Autowired private UserRepository repo; @RequestMapping(method = RequestMethod.GET, value = "/users") @ResponseBody public List<User> search(@RequestParam(value = "search") String search) { UserSpecificationsBuilder builder = new UserSpecificationsBuilder(); 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)); } Specification<User> spec = builder.build(); return repo.findAll(spec); } }
درخواست کوئری کلاینت :
http://localhost:8080/users?search=lastName:doe,age>25
نمونه جواب دریافتی از کنترلر :
[{ "id":2, "firstName":"tom", "lastName":"doe", "email":"tom@doe.com", "age":26 }]
* نکته : از آنجا که مقادیر مورد نیاز برای فیلتر کرد توسط کاما جدا میشود ما نمیتوانیم این کاراکتر را جز موارد فیلتر شونده قرار دهیم چون در Pattern از آن به عنوان splitter استفاده شده است و چنانچه بخواهیم کاما جز موارد فیلتر شونده باشد میتوانیم کاراکتر splitter را به کاراکتر دیگری تغییر دهیم مثلا سمی کالمن