RESTクエリ言語 - 高度な検索操作

1概要

この記事では、 前の部分 で開発したRESTクエリー言語を拡張します。 language-tutorial[シリーズ]に より多くの検索操作を含める

等号、否定、大なり、小なり、で始まる、で終わる、含む、および類似の操作がサポートされるようになりました。

JPA基準、Spring Data JPA仕様、およびQuery DSLという3つの実装を調査したことに注意してください。この記事では、仕様を明確にして柔軟な方法で表現しています。

2 SearchOperation enum

まず、列挙を使用して、サポートされているさまざまな検索操作をより適切に表現することから始めましょう。

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

2組の操作があります。

{空} 1 単純 - 1文字で表すことができます。

  • 等号:コロン( : )で表します

  • 否定:感嘆符( )で表します

  • より大きい:( > )で表される

  • より小さい:( < )で表される

  • Like:チルダ( )で表される

{空} 2。 複雑 - 表現するには複数の文字が必要です。

  • で始まる:( =プレフィックス** )で表される

  • で終わる:( = ** サフィックス

  • 含む:( = 部分文字列 )で表される

また、新しい SearchOperation を使用するように SearchCriteria クラスを変更する必要があります。

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

3 UserSpecification を変更します

それでは、新しくサポートされたオペレーションを UserSpecification 実装に含めましょう。

public class UserSpecification implements Specification<User> {

    private SearchCriteria criteria;

    @Override
    public Predicate toPredicate(
      Root<User> 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.<String> get(
              criteria.getKey()), criteria.getValue().toString());
        case LESS__THAN:
            return builder.lessThan(root.<String> get(
              criteria.getKey()), criteria.getValue().toString());
        case LIKE:
            return builder.like(root.<String> get(
              criteria.getKey()), criteria.getValue().toString());
        case STARTS__WITH:
            return builder.like(root.<String> get(criteria.getKey()), criteria.getValue() + "%");
        case ENDS__WITH:
            return builder.like(root.<String> get(criteria.getKey()), "%" + criteria.getValue());
        case CONTAINS:
            return builder.like(root.<String> get(
              criteria.getKey()), "%" + criteria.getValue() + "%");
        default:
            return null;
        }
    }
}

4持続性テスト

次に、新しい検索操作を持続性レベルでテストしましょう。

** 4.1. テストの均等性

**

次の例では、姓名でユーザーを検索します。

@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<User> results = repository.findAll(Specification.where(spec).and(spec1));

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

4.2. テスト否定

次に、 自分の名前で“ john” ではないユーザーを検索しましょう。

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

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

** 4.3. より大きいテスト

**

次に - 年齢が「25」 を超えるユーザーを検索します。

@Test
public void givenMinAge__whenGettingListOfUsers__thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("age", SearchOperation.GREATER__THAN, "25"));
    List<User> results = repository.findAll(Specification.where(spec));

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

** 4.4. テスト開始

**

Next - 「jo」で始まる名前が** のユーザー

@Test
public void givenFirstNamePrefix__whenGettingListOfUsers__thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.STARTS__WITH, "jo"));
    List<User> results = repository.findAll(spec);

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

** 4.5. テストは+で終了

**

次に、 の名前が「n」 で終わるユーザーを検索します。

@Test
public void givenFirstNameSuffix__whenGettingListOfUsers__thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.ENDS__WITH, "n"));
    List<User> results = repository.findAll(spec);

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

** 4.6. テスト内容

さて、私たちは** 自分の名前に "oh"を含むユーザーを検索します。

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

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

** 4.7. テスト範囲

**

最後に、年齢が「20」から「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<User> results = repository.findAll(Specification.where(spec).and(spec1));

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

5 UserSpecificationBuilder

永続化が行われてテストされたので、今度はWeb層に注意を向けましょう。

前回の記事の UserSpecificationBuilder 実装をベースにして、新しい新しい検索操作を組み込みます** 。

public class UserSpecificationsBuilder {

    private List<SearchCriteria> 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<User> 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. UserController

次に、 UserController を修正して、新しい操作を正しく 解析する 必要があります。

@RequestMapping(method = RequestMethod.GET, value = "/users")
@ResponseBody
public List<User> 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<User> spec = builder.build();
    return dao.findAll(spec);
}

これでAPIを使用して、任意の組み合わせの基準で正しい結果を取り戻すことができます。例えば、これは複雑な操作がクエリ言語を持つAPIを使っているように見えるものです:

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

そして応答:

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

7. 検索APIのテスト

最後に、一連のAPIテストを作成して、APIが正しく機能することを確認しましょう。

テストの簡単な構成とデータの初期化から始めましょう。

@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. テストの均等性

**

まず - 名が " john "、名が " 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. テスト否定

**

今すぐ - 私たちは、** 彼らの名前が "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. より大きいテスト

**

次に - 年齢が「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. テスト開始

**

次に - 自分の名前が「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. テスト終了

**

今すぐ - 名が "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. テスト内容

次に、** 自分の名前に "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. テスト範囲

**

最後に、年齢が「20」から「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結論

この記事では、私たちのREST検索APIのクエリー言語を、成熟した、テストされた、プロダクショングレードの実装へと導きました。これで、さまざまな操作や制約がサポートされるようになりました。これにより、あらゆるデータセットをエレガントに切り取り、探している正確なリソースにアクセスすることが非常に簡単になります。

この記事の 完全な実装 はhttps://github.com/eugenp/tutorials/tree/master/spring-rest-query-language[the GitHub project]にあります - これはMavenベースのプロジェクトなので、インポートしてそのまま実行するのは簡単なはずです。

"

  • «** 前へ