REST-Abfragesprache - Erweiterte Suchvorgänge

REST-Abfragesprache - Erweiterte Suchvorgänge

1. Überblick

In diesem Artikel erweitern wir die von uns entwickelte REST-Abfragesprache inthe previous parts vonthe series aufinclude more search operations.

Wir unterstützen jetzt die folgenden Operationen: Gleichheit, Negation, Größer als, Kleiner als, Beginnt mit, Endet mit, Enthält und Ähnliches.

Beachten Sie, dass wir drei Implementierungen untersucht haben: JPA-Kriterien, Spring Data JPA-Spezifikationen und Abfrage-DSL; Wir werden mit den Spezifikationen in diesem Artikel fortfahren, da dies eine saubere und flexible Möglichkeit ist, unsere Abläufe darzustellen.

2. DieSearchOperationenum

Zunächst definieren wir zunächst eine bessere Darstellung unserer verschiedenen unterstützten Suchvorgänge über eine Aufzählung:

public enum SearchOperation {
    EQUALITY, NEGATION, GREATER_THAN, LESS_THAN, LIKE, STARTS_WITH, ENDS_WITH, CONTAINS;

    public static final String[] SIMPLE_OPERATION_SET = { ":", "!", ">", "<", "~" };

    public static SearchOperation getSimpleOperation(char input) {
        switch (input) {
        case ':':
            return EQUALITY;
        case '!':
            return NEGATION;
        case '>':
            return GREATER_THAN;
        case '<':
            return LESS_THAN;
        case '~':
            return LIKE;
        default:
            return null;
        }
    }
}

Wir haben zwei Arten von Operationen:

1. Simple - kann durch ein Zeichen dargestellt werden:

  • Gleichheit: dargestellt durch Doppelpunkt (:)

  • Negation: dargestellt durch Ausrufezeichen (!)

  • Größer als: dargestellt durch (>)

  • Weniger als: dargestellt durch (<)

  • Wie: dargestellt durch Tilde (~)

2. Complex - Es muss mehr als ein Zeichen dargestellt werden:

  • Beginnt mit: dargestellt durch (=prefix*)

  • Endet mit: dargestellt durch (=*suffix)

  • Enthält: dargestellt durch (=*substring*)

Wir müssen auch unsereSearchCriteria-Klasse ändern, um die neuenSearchOperation zu verwenden:

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

3. Ändern SieUserSpecification

Nehmen wir nun die neu unterstützten Operationen in die Implementierung vonUserSpecificationauf:

public class UserSpecification implements Specification {

    private SearchCriteria criteria;

    @Override
    public Predicate toPredicate(
      Root root, CriteriaQuery query, CriteriaBuilder builder) {

        switch (criteria.getOperation()) {
        case EQUALITY:
            return builder.equal(root.get(criteria.getKey()), criteria.getValue());
        case NEGATION:
            return builder.notEqual(root.get(criteria.getKey()), criteria.getValue());
        case GREATER_THAN:
            return builder.greaterThan(root. get(
              criteria.getKey()), criteria.getValue().toString());
        case LESS_THAN:
            return builder.lessThan(root. get(
              criteria.getKey()), criteria.getValue().toString());
        case LIKE:
            return builder.like(root. get(
              criteria.getKey()), criteria.getValue().toString());
        case STARTS_WITH:
            return builder.like(root. get(criteria.getKey()), criteria.getValue() + "%");
        case ENDS_WITH:
            return builder.like(root. get(criteria.getKey()), "%" + criteria.getValue());
        case CONTAINS:
            return builder.like(root. get(
              criteria.getKey()), "%" + criteria.getValue() + "%");
        default:
            return null;
        }
    }
}

4. Persistenztests

Als Nächstes testen wir unsere neuen Suchvorgänge auf Persistenzniveau:

4.1. Gleichheit testen

Im folgenden Beispiel suchen wir nach einem Benutzerby their first and last name:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.EQUALITY, "john"));
    UserSpecification spec1 = new UserSpecification(
      new SearchCriteria("lastName", SearchOperation.EQUALITY, "doe"));
    List results = repository.findAll(Specification.where(spec).and(spec1));

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

4.2. Test Negation

Als nächstes suchen wir nach Benutzern, die nachtheir first name not “john”:

@Test
public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.NEGATION, "john"));
    List results = repository.findAll(Specification.where(spec));

    assertThat(userTom, isIn(results));
    assertThat(userJohn, not(isIn(results)));
}

4.3. Test größer als

Als nächstes suchen wir nach Benutzern mitage greater than “25”:

@Test
public void givenMinAge_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("age", SearchOperation.GREATER_THAN, "25"));
    List results = repository.findAll(Specification.where(spec));

    assertThat(userTom, isIn(results));
    assertThat(userJohn, not(isIn(results)));
}

4.4. Test beginnt mit

Weiter - Benutzer mittheir first name starting with “jo”:

@Test
public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.STARTS_WITH, "jo"));
    List results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

4.5. Test endet mit

Als Nächstes suchen wir nach Benutzern mittheir first name ending with “n”:

@Test
public void givenFirstNameSuffix_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.ENDS_WITH, "n"));
    List results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

4.6. Test enthält

Jetzt suchen wir nach Benutzern mittheir first name containing “oh”:

@Test
public void givenFirstNameSubstring_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.CONTAINS, "oh"));
    List results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

4.7. Testbereich

Schließlich suchen wir nach Benutzern mitages between “20” and “25”:

@Test
public void givenAgeRange_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("age", SearchOperation.GREATER_THAN, "20"));
    UserSpecification spec1 = new UserSpecification(
      new SearchCriteria("age", SearchOperation.LESS_THAN, "25"));
    List results = repository.findAll(Specification.where(spec).and(spec1));

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

5. DieUserSpecificationBuilder

Nachdem die Persistenz durchgeführt und getestet wurde, lenken wir unsere Aufmerksamkeit auf die Webebene.

Wir bauen auf der Implementierung vonUserSpecificationBuilderaus dem vorherigen Artikel aufincorporate the new new search operations auf:

public class UserSpecificationsBuilder {

    private List params;

    public UserSpecificationsBuilder with(
      String key, String operation, Object value, String prefix, String suffix) {

        SearchOperation op = SearchOperation.getSimpleOperation(operation.charAt(0));
        if (op != null) {
            if (op == SearchOperation.EQUALITY) {
                boolean startWithAsterisk = prefix.contains("*");
                boolean endWithAsterisk = suffix.contains("*");

                if (startWithAsterisk && endWithAsterisk) {
                    op = SearchOperation.CONTAINS;
                } else if (startWithAsterisk) {
                    op = SearchOperation.ENDS_WITH;
                } else if (endWithAsterisk) {
                    op = SearchOperation.STARTS_WITH;
                }
            }
            params.add(new SearchCriteria(key, op, value));
        }
        return this;
    }

    public Specification build() {
        if (params.size() == 0) {
            return null;
        }

        Specification result = new UserSpecification(params.get(0));

        for (int i = 1; i < params.size(); i++) {
            result = params.get(i).isOrPredicate()
              ? Specification.where(result).or(new UserSpecification(params.get(i)))
              : Specification.where(result).and(new UserSpecification(params.get(i)));
        }

        return result;
    }
}

6. DieUserController

Als nächstes müssen wir unsereUserController auf korrektparse the new operations ändern:

@RequestMapping(method = RequestMethod.GET, value = "/users")
@ResponseBody
public List findAllBySpecification(@RequestParam(value = "search") String search) {
    UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
    String operationSetExper = Joiner.on("|").join(SearchOperation.SIMPLE_OPERATION_SET);
    Pattern pattern = Pattern.compile(
      "(\\w+?)(" + operationSetExper + ")(\p{Punct}?)(\\w+?)(\p{Punct}?),");
    Matcher matcher = pattern.matcher(search + ",");
    while (matcher.find()) {
        builder.with(
          matcher.group(1),
          matcher.group(2),
          matcher.group(4),
          matcher.group(3),
          matcher.group(5));
    }

    Specification spec = builder.build();
    return dao.findAll(spec);
}

Wir können jetzt die API aufrufen und mit jeder Kombination von Kriterien die richtigen Ergebnisse erzielen. Beispiel: So würde eine komplexe Operation mit der API mit der Abfragesprache aussehen:

http://localhost:8080/users?search=firstName:jo*,age<25

Und die Antwort:

[{
    "id":1,
    "firstName":"john",
    "lastName":"doe",
    "email":"[email protected]",
    "age":24
}]

7. Tests für die Such-API

Stellen Sie schließlich sicher, dass unsere API gut funktioniert, indem Sie eine Reihe von API-Tests schreiben.

Wir beginnen mit der einfachen Konfiguration des Tests und der Dateninitialisierung:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  classes = { ConfigTest.class, PersistenceConfig.class },
  loader = AnnotationConfigContextLoader.class)
@ActiveProfiles("test")
public class JPASpecificationLiveTest {

    @Autowired
    private UserRepository repository;

    private User userJohn;
    private User userTom;

    private final String URL_PREFIX = "http://localhost:8080/users?search=";

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

    private RequestSpecification givenAuth() {
        return RestAssured.given().auth()
                                  .preemptive()
                                  .basic("username", "password");
    }
}

7.1. Gleichheit testen

Zuerst suchen wir nach einem Benutzer mitthe first name “john” and last name “doe:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName:john,lastName:doe");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

7.2. Test Negation

Jetzt suchen wir nach Benutzern, wenntheir first name isn’t “john”:

@Test
public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName!john");
    String result = response.body().asString();

    assertTrue(result.contains(userTom.getEmail()));
    assertFalse(result.contains(userJohn.getEmail()));
}

7.3. Test größer als

Als nächstes suchen wir nach Benutzern mitage greater than “25”:

@Test
public void givenMinAge_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "age>25");
    String result = response.body().asString();

    assertTrue(result.contains(userTom.getEmail()));
    assertFalse(result.contains(userJohn.getEmail()));
}

7.4. Test beginnt mit

Weiter - Benutzer mittheir first name starting with “jo”:

@Test
public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName:jo*");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

7.5. Test endet mit

Jetzt - Benutzer mittheir first name ending with “n”:

@Test
public void givenFirstNameSuffix_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName:*n");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

7.6. Test enthält

Als Nächstes suchen wir nach Benutzern mittheir first name containing “oh”:

@Test
public void givenFirstNameSubstring_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName:*oh*");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

7.7. Testbereich

Schließlich suchen wir nach Benutzern mitages between “20” and “25”:

@Test
public void givenAgeRange_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "age>20,age<25");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

8. Fazit

In diesem Artikel haben wir die Abfragesprache unserer REST-Such-API aufa mature, tested, production-grade implementation weitergeleitet. Wir unterstützen jetzt eine Vielzahl von Vorgängen und Einschränkungen, die es recht einfach machen sollten, jeden Datensatz elegant zu durchschneiden und genau die Ressourcen zu finden, nach denen wir suchen.

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.