یکی از عملیات معمول در API ها search و filter کردن ریسورس ها بر اساس فیلد هایشان است اما این میتواند پیچیدگی های زیادی را روی طراحی اعمال کند و همینطور بهینه نخواهد بود
بهترین روش داشتن یک زبان منعطف برای کوئری زدن روی API است
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age; }
فیلتر کردن با استفاده از CriteriaBuilder :
در کد زیر میتوانیم ببینیم که چطور با Criteria میتوان کوئری روی ریسورس را انعطاف پذیر تر کنیم
@Repository public class UserDAO implements IUserDAO { @PersistenceContext private EntityManager entityManager; @Override public List<User> searchUser(List<SearchCriteria> params) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = builder.createQuery(User.class); Root r = query.from(User.class); Predicate predicate = builder.conjunction(); UserSearchQueryCriteriaConsumer searchConsumer = new UserSearchQueryCriteriaConsumer(predicate, builder, r); params.stream().forEach(searchConsumer); predicate = searchConsumer.getPredicate(); query.where(predicate); List<User> result = entityManager.createQuery(query).getResultList(); return result; } @Override public void save(User entity) { entityManager.persist(entity); } }
در کد زیر میبینیم که چطور از پارامتر های ورودی و Criteria یک کوئری ساخته ایم:
public class UserSearchQueryCriteriaConsumer implements Consumer<SearchCriteria>{ private Predicate predicate; private CriteriaBuilder builder; private Root r; @Override public void accept(SearchCriteria param) { if (param.getOperation().equalsIgnoreCase(">")) { predicate = builder.and(predicate, builder .greaterThanOrEqualTo(r.get(param.getKey()), param.getValue().toString())); } else if (param.getOperation().equalsIgnoreCase("<")) { predicate = builder.and(predicate, builder.lessThanOrEqualTo( r.get(param.getKey()), param.getValue().toString())); } else if (param.getOperation().equalsIgnoreCase(":")) { if (r.get(param.getKey()).getJavaType() == String.class) { predicate = builder.and(predicate, builder.like( r.get(param.getKey()), "%" + param.getValue() + "%")); } else { predicate = builder.and(predicate, builder.equal( r.get(param.getKey()), param.getValue())); } } } // standard constructor, getter, setter }
کلاسی که پارامترهای کوئری را میتوان در آن جای داد به سادگی کلاس زیر است :
public class SearchCriteria { private String key; private String operation; private Object value; }
key نام فیلد را نگه داری میکند مانند "firstName"
operation عملیات درخواستی را مشخص میکند
value مقداری که عملیات روی فیلد بر اساس آن انجام میشود
تست :
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { PersistenceConfig.class }) @Transactional @TransactionConfiguration public class JPACriteriaQueryTest { @Autowired private IUserDAO userApi; 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); userApi.save(userJohn); userTom = new User(); userTom.setFirstName("Tom"); userTom.setLastName("Doe"); userTom.setEmail("tom@doe.com"); userTom.setAge(26); userApi.save(userTom); } }
در کد بالا ابتدا دو ابجکت از User ساخته ایم و در مرحله بعد کد اصلی تست را مینویسیم در این تست کوئری را بر اساس User هایی که firstName با مقدار John و lastName با مقدار Doe را میخواهیم فیلتر کنیم
@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("firstName", ":", "John")); params.add(new SearchCriteria("lastName", ":", "Doe")); List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
یک تست دیگر که کوئری بر اساس تنها lastName :
@Test public void givenLast_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("lastName", ":", "Doe")); List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, isIn(results)); }
تستی دیگر که کوئری آن ریسورس های با age بزرگتر یا مساوی 25 را فیلتر میکند :
@Test public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("lastName", ":", "Doe")); params.add(new SearchCriteria("age", ">", "25")); List<User> results = userApi.searchUser(params); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }
کوئری که هیچ چیزی برابر فیلد ها مقادیر ارسال شده وجود ندارد و چیزی برنمیگرداند:
@Test public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("firstName", ":", "Adam")); params.add(new SearchCriteria("lastName", ":", "Fox")); List<User> results = userApi.searchUser(params); assertThat(userJohn, not(isIn(results))); assertThat(userTom, not(isIn(results))); }
تست کوئری بر اساس تنها firstName :
@Test public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("firstName", ":", "jo")); List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
تست واقعی بر اساس مدل واقعی که دیتابیس وجود دارد و کوئری ها را کلاینت از راه دور درخواست و توسط یک مت کنترلر دریافت و پاسخ ساخته میشود :
http://localhost:8080/users?search=lastName:doe,age>25
@Controller public class UserController { @Autowired private IUserDao api; @RequestMapping(method = RequestMethod.GET, value = "/users") @ResponseBody public List<User> findAll(@RequestParam(value = "search", required = false) String search) { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); if (search != null) { Pattern pattern = Pattern.compile("(\w+?)(:|<|>)(\w+?),"); Matcher matcher = pattern.matcher(search + ","); while (matcher.find()) { params.add(new SearchCriteria(matcher.group(1), matcher.group(2), matcher.group(3))); } } return api.searchUser(params); } }