Bahasa Pertanyaan REST dengan Spesifikasi JPA Spring Data

Artikel ini adalah sebahagian daripada siri: • REST Query Language dengan Kriteria Spring dan JPA

• Bahasa Pertanyaan REST dengan Spesifikasi JPA Spring Data (artikel semasa) • Bahasa Pertanyaan REST dengan JPA Spring Data dan Querydsl

• REST Query Language - Operasi Carian Lanjutan

• Bahasa Pertanyaan REST - Melaksanakan ATAU Operasi

• REST Query Language dengan RSQL

• REST Query Language dengan Sokongan Web Querydsl

1. Gambaran keseluruhan

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

Kami mula melihat bahasa pertanyaan dalam artikel pertama siri ini - dengan penyelesaian berdasarkan Kriteria JPA.

Jadi - mengapa bahasa pertanyaan? Kerana - untuk apa-apa API yang cukup kompleks - mencari / menapis sumber anda dengan medan yang sangat mudah tidak mencukupi. Bahasa pertanyaan lebih fleksibel dan membolehkan anda menapis sumber yang anda perlukan.

2. Entiti Pengguna

Pertama - mari mulakan dengan entiti Pengguna yang mudah untuk API Carian kami:

@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. Tapis Menggunakan Spesifikasi

Sekarang - mari masuk ke bahagian yang paling menarik dari masalah ini - bertanya dengan Spesifikasi JPA Spring Data tersuai .

Kami akan membuat Spesifikasi Pengguna yang melaksanakan antara muka Spesifikasi dan kami akan melewati kekangan kami sendiri untuk membina pertanyaan sebenar :

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 dapat kita lihat - kita membuat Spesifikasi berdasarkan beberapa batasan sederhana yang kita wakili dalam kelas " SearchCriteria " berikut :

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

The SearchCriteria pelaksanaan memegang perwakilan asas kekangan - dan ia berdasarkan kekangan ini yang kita akan dapat membina pertanyaan:

  • kunci : nama medan - contohnya, nama pertama , umur , ... dll.
  • operasi : operasi - contohnya, persamaan, kurang daripada, ... dll.
  • nilai : nilai medan - contohnya, john, 25,… dll.

Sudah tentu, pelaksanaannya sederhana dan boleh diperbaiki; ia bagaimanapun merupakan asas yang kukuh untuk operasi yang kuat dan fleksibel yang kita perlukan.

4. Penyimpanan Pengguna

Seterusnya - mari lihat UserRepository ; kami hanya memperluaskan JpaSpecificationExecutor untuk mendapatkan API Spesifikasi baru:

public interface UserRepository extends JpaRepository, JpaSpecificationExecutor {}

5. Uji Pertanyaan Carian

Sekarang - mari kita uji API carian baru.

Pertama, mari buat beberapa pengguna untuk menyiapkannya semasa ujian 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); } }

Seterusnya, mari kita lihat bagaimana mencari 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 mencari pengguna dengan nama depan dan belakang :

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

Seterusnya, mari kita lihat bagaimana mencari pengguna dengan nama belakang dan umur minimum :

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

Akhirnya - mari lihat bagaimana mencari Pengguna yang diberi hanya sebahagian daripada nama pertama :

@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

Seterusnya - mari kita lihat menggabungkan Spesifikasi khusus kita untuk menggunakan pelbagai kekangan dan menapis mengikut pelbagai kriteria.

Kami akan melaksanakan pembangun - UserSpecificationsBuilder - untuk menggabungkan Spesifikasi dengan mudah dan lancar :

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. Pengawal Pengguna

Akhirnya - mari gunakan fungsi carian / penapis ketekunan baru ini dan sediakan REST API - dengan membuat UserController dengan operasi carian mudah :

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

Perhatikan bahawa untuk menyokong sistem bukan Inggeris yang lain, objek Pola dapat diubah seperti:

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

Berikut adalah contoh URL ujian untuk menguji API:

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

Dan tindak balas:

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

Oleh kerana carian dibahagi dengan “,” dalam contoh Corak kami , istilah carian tidak boleh mengandungi watak ini. Coraknya juga tidak sesuai dengan ruang kosong.

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.

Seterusnya » Bahasa Pertanyaan REST dengan Spring Data JPA dan Querydsl « Bahasa Pertanyaan REST sebelumnya dengan Kriteria Spring dan JPA