Bahasa Kueri REST dengan Spesifikasi JPA Spring Data

Artikel ini adalah bagian dari serial: • REST Query Language dengan Kriteria Spring dan JPA

• Bahasa Kueri REST dengan Spesifikasi JPA Spring Data (artikel saat ini) • Bahasa Kueri REST dengan Spring Data JPA dan Querydsl

• REST Query Language - Operasi Pencarian Lanjutan

• REST Query Language - Menerapkan ATAU Operasi

• REST Query Language dengan RSQL

• REST Query Language dengan Querydsl Web Support

1. Ikhtisar

Dalam tutorial ini - kita akan membuat Search / Filter REST API menggunakan Spring Data JPA dan Spesifikasi.

Kami mulai melihat bahasa kueri di artikel pertama seri ini - dengan solusi berbasis Kriteria JPA.

Jadi - mengapa bahasa kueri? Karena - untuk API yang cukup kompleks - mencari / memfilter sumber daya Anda dengan bidang yang sangat sederhana tidaklah cukup. Bahasa kueri lebih fleksibel dan memungkinkan Anda memfilter sumber daya yang Anda butuhkan.

2. Entitas Pengguna

Pertama - mari kita mulai dengan entitas Pengguna sederhana untuk API Pencarian kita:

@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 }

3. Filter Menggunakan Spesifikasi

Sekarang - mari kita langsung ke bagian masalah yang paling menarik - membuat kueri dengan Spesifikasi JPA Data Musim Semi kustom .

Kami akan membuat UserSpecification yang mengimplementasikan antarmuka Spesifikasi dan kami akan meneruskan batasan kami sendiri untuk membuat kueri yang sebenarnya :

public class UserSpecification implements Specification { private SearchCriteria criteria; @Override public Predicate toPredicate (Root root, CriteriaQuery query, CriteriaBuilder builder) { if (criteria.getOperation().equalsIgnoreCase(">")) { return builder.greaterThanOrEqualTo( root. get(criteria.getKey()), criteria.getValue().toString()); } else if (criteria.getOperation().equalsIgnoreCase("<")) { return builder.lessThanOrEqualTo( root. get(criteria.getKey()), criteria.getValue().toString()); } else if (criteria.getOperation().equalsIgnoreCase(":")) { if (root.get(criteria.getKey()).getJavaType() == String.class) { return builder.like( root.get(criteria.getKey()), "%" + criteria.getValue() + "%"); } else { return builder.equal(root.get(criteria.getKey()), criteria.getValue()); } } return null; } }

Seperti yang bisa kita lihat - kita membuat Spesifikasi berdasarkan beberapa batasan sederhana yang kita wakili dalam kelas " Kriteria Pencarian " berikut :

public class SearchCriteria { private String key; private String operation; private Object value; }

The SearchCriteria pelaksanaan memegang representasi dasar kendala - dan hal ini didasarkan pada kendala ini bahwa kita akan membangun query:

  • key : nama field - misalnya, firstName , age ,… dll.
  • operasi : operasi - misalnya, kesetaraan, kurang dari,… dll.
  • nilai : nilai bidang - misalnya, john, 25,… dll.

Tentu saja, implementasinya sederhana dan bisa diperbaiki; namun ini merupakan dasar yang kokoh untuk operasi yang kuat dan fleksibel yang kami butuhkan.

4. UserRepository

Selanjutnya - mari kita lihat UserRepository ; kami hanya memperluas JpaSpecificationExecutor untuk mendapatkan API Spesifikasi baru:

public interface UserRepository extends JpaRepository, JpaSpecificationExecutor {}

5. Uji Kueri Pencarian

Sekarang - mari kita uji API pencarian baru.

Pertama, mari buat beberapa pengguna agar mereka siap saat pengujian dijalankan:

@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("[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); } }

Selanjutnya, mari kita lihat bagaimana menemukan pengguna dengan nama belakang yang diberikan :

@Test public void givenLast_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification(new SearchCriteria("lastName", ":", "doe")); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, isIn(results)); }

Sekarang, mari kita lihat bagaimana menemukan pengguna dengan nama depan dan belakang yang diberikan :

@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { UserSpecification spec1 = new UserSpecification(new SearchCriteria("firstName", ":", "john")); UserSpecification spec2 = new UserSpecification(new SearchCriteria("lastName", ":", "doe")); List results = repository.findAll(Specification.where(spec1).and(spec2)); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

Catatan: Kami menggunakan "di mana " dan " dan " untuk menggabungkan Spesifikasi .

Selanjutnya, mari kita lihat cara menemukan pengguna dengan nama belakang dan usia minimum yang diberikan :

@Test public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() { UserSpecification spec1 = new UserSpecification(new SearchCriteria("age", ">", "25")); UserSpecification spec2 = new UserSpecification(new SearchCriteria("lastName", ":", "doe")); List results = repository.findAll(Specification.where(spec1).and(spec2)); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }

Sekarang, mari kita lihat cara mencari Pengguna yang sebenarnya tidak ada :

@Test public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { UserSpecification spec1 = new UserSpecification(new SearchCriteria("firstName", ":", "Adam")); UserSpecification spec2 = new UserSpecification(new SearchCriteria("lastName", ":", "Fox")); List results = repository.findAll(Specification.where(spec1).and(spec2)); assertThat(userJohn, not(isIn(results))); assertThat(userTom, not(isIn(results))); }

Terakhir - mari kita lihat bagaimana menemukan Pengguna yang hanya diberikan sebagian dari nama depan :

@Test public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification(new SearchCriteria("firstName", ":", "jo")); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

6. Gabungkan Spesifikasi

Berikutnya - mari kita lihat menggabungkan Spesifikasi khusus untuk menggunakan beberapa batasan dan filter sesuai dengan beberapa kriteria.

We're going to implement a builder – UserSpecificationsBuilder – to easily and fluently combine Specifications:

public class UserSpecificationsBuilder { private final List params; public UserSpecificationsBuilder() { params = new ArrayList(); } public UserSpecificationsBuilder with(String key, String operation, Object value) { params.add(new SearchCriteria(key, operation, value)); return this; } public Specification build() { if (params.size() == 0) { return null; } List 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; } }

7. UserController

Finally – let's use this new persistence search/filter functionality and set up the REST API – by creating a UserController with a simple search operation:

@Controller public class UserController { @Autowired private UserRepository repo; @RequestMapping(method = RequestMethod.GET, value = "/users") @ResponseBody public List 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 spec = builder.build(); return repo.findAll(spec); } }

Note that to support other non-English systems, the Pattern object could be changed like:

Pattern pattern = Pattern.compile("(\\w+?)(:|)(\\w+?),", Pattern.UNICODE_CHARACTER_CLASS);

Here is a test URL example to test out the API:

//localhost:8080/users?search=lastName:doe,age>25

And the response:

[{ "id":2, "firstName":"tom", "lastName":"doe", "email":"[email protected]", "age":26 }]

Since the searches are split by a “,” in our Pattern example, the search terms can't contain this character. The pattern also doesn't match whitespace.

If we want to search for values containing commas, then we can consider using a different separator such as “;”.

Another option would be to change the pattern to search for values between quotes, then strip these from the search term:

Pattern pattern = Pattern.compile("(\\w+?)(:|)(\"([^\"]+)\")");

8. Conclusion

This tutorial covered a simple implementation that can be the base of a powerful REST query language. We've made good use of Spring Data Specifications to make sure we keep the API away from the domain and have the option to handle many other types of operations.

The full implementation of this article can be found in the GitHub project – this is a Maven-based project, so it should be easy to import and run as it is.

Berikutnya » Bahasa Kueri REST dengan Spring Data JPA dan Querydsl « Bahasa Kueri REST Sebelumnya dengan Kriteria Spring dan JPA