REST-Abfragesprache mit Spring Data JPA-Spezifikationen

REST-Abfragesprache mit JPA-Spezifikationen für Federdaten

1. Überblick

In diesem Tutorial erstellen wir einSearch/Filter REST API unter Verwendung von Spring Data JPA und Spezifikationen.

Wir haben begonnen, eine Abfragesprache infirst article vonthis series zu untersuchen - mit einer auf JPA-Kriterien basierenden Lösung.

Also -why a query language? Weil - für jede ausreichend komplexe API - das Suchen / Filtern Ihrer Ressourcen nach sehr einfachen Feldern einfach nicht ausreicht. A query language is more flexible und ermöglicht es Ihnen, auf genau die Ressourcen zu filtern, die Sie benötigen.

2. User Entität

Beginnen wir zunächst mit einer einfachenUser-Entität für unsere Such-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;

    // standard getters and setters
}

3. Filtern mitSpecification

Kommen wir nun zum interessantesten Teil des Problems - der Abfrage mit benutzerdefinierten Spring Data JPASpecifications.

Wir erstellen einUserSpecification, das dieSpecification-Schnittstelle implementiert, und wir gehen zupass in our own constraint to construct the actual query:

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

Wie wir sehen können -we create a Specification based on some simple constrains, die wir in der folgenden Klasse "SearchCriteria" darstellen:

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

Die Implementierung vonSearchCriteriaenthält eine grundlegende Darstellung einer Einschränkung - und basierend auf dieser Einschränkung werden wir die Abfrage erstellen:

  • key: Der Feldname - zum BeispielfirstName,age usw.

  • operation: die Operation - zum Beispiel Gleichheit, kleiner als, ... usw.

  • value: der Feldwert - zum Beispiel John, 25,… usw.

Natürlich ist die Implementierung einfach und kann verbessert werden. Es ist jedoch eine solide Basis für die leistungsstarken und flexiblen Operationen, die wir benötigen.

4. DieUserRepository

Weiter - Schauen wir uns dieUserRepositoryan. Wir erweitern einfach dieJpaSpecificationExecutor, um die neuen Spezifikations-APIs zu erhalten:

public interface UserRepository
  extends JpaRepository, JpaSpecificationExecutor {}

5. Testen Sie die Suchanfragen

Testen Sie jetzt die neue Such-API.

Lassen Sie uns zunächst einige Benutzer erstellen, um sie bereit zu halten, wenn die Tests ausgeführt werden:

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

Als nächstes sehen wir uns an, wie Sie Benutzer mitgiven last name finden:

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

Lassen Sie uns nun sehen, wie Sie einen Benutzer mit bestimmtenboth first and last name finden:

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

Hinweis: Wir haben "where" und "and" biscombine Specifications verwendet.

Als nächstes sehen wir uns an, wie Sie einen Benutzer mit bestimmtenboth last name and minimum age finden:

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

Lassen Sie uns nun sehen, wie Sie nachUser suchen, diedoesn’t actually exist:

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

Zum Schluss sehen wir uns an, wie SieUsergiven only part of the first name finden:

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

Weiter - Schauen wir uns an, wie Sie unsere benutzerdefiniertenSpecificationskombinieren, um mehrere Einschränkungen zu verwenden und nach mehreren Kriterien zu filtern.

Wir werden einen Builder implementieren -UserSpecificationsBuilder -, umSpecifications einfach und flüssig zu kombinieren:

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

Verwenden Sie zum Schluss diese neue Persistenzsuch- / Filterfunktion undset up the REST API, indem Sie mit einer einfachensearch-Operation einUserController erstellen:

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

Beachten Sie, dass zur Unterstützung anderer nicht englischer Systeme das ObjektPatternwie folgt geändert werden kann:

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

Hier ist ein Test-URL-Beispiel zum Testen der API:

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

Und die Antwort:

[{
    "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. Das Muster stimmt auch nicht mit Leerzeichen überein.

Wenn Sie nach Werten suchen möchten, die Kommas enthalten, können Sie ein anderes Trennzeichen wie „;“ verwenden.

Eine andere Möglichkeit wäre, das Muster zu ändern, um zwischen Anführungszeichen nach Werten zu suchen, und diese dann aus dem Suchbegriff zu streichen:

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

8. Fazit

In diesem Lernprogramm wurde eine einfache Implementierung behandelt, die als Grundlage für eine leistungsstarke REST-Abfragesprache dienen kann. Wir haben Spring Data Specifications gut genutzt, um sicherzustellen, dass die API von der Domäne undhave the option to handle many other types of operations ferngehalten wird.

Diefull implementation dieses Artikels befinden sich inthe GitHub project - dies ist ein Maven-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.