1. 概要

このチュートリアルでは、Spring DataJPAと仕様を使用して検索/フィルターRESTAPIを構築します。

このシリーズ最初の記事で、JPA基準ベースのソリューションを使用してクエリ言語を調べ始めました。

そう、 なぜクエリ言語なのか? 非常に単純なフィールドでリソースを検索/フィルタリングするだけでは、複雑すぎるAPIには不十分だからです。 クエリ言語はより柔軟で、必要なリソースに正確に絞り込むことができます。

2. ユーザーエンティティ

まず、検索APIの単純なUserエンティティから始めましょう。

@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. 仕様を使用したフィルター

次に、問題の最も興味深い部分、つまりカスタムSpring DataJPA仕様を使用したクエリについて説明します。

Specificationインターフェースを実装するUserSpecificationを作成し、独自の制約を渡して実際のクエリを作成します。

public class UserSpecification implements Specification<User> {

    private SearchCriteria criteria;

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

ご覧のとおり、次のSearchCriteriaクラスで表すいくつかの単純な制約に基づいて仕様を作成します。

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

SearchCriteria 実装は、制約の基本的な表現を保持し、この制約に基づいてクエリを作成します。

  • key :フィールド名。たとえば、 firstName ageなど。
  • operation :操作、たとえば、等式、より小さいなど。
  • value :フィールド値(たとえば、john、25など)。

もちろん、実装は単純であり、改善することができます。 ただし、これは、必要な強力で柔軟な操作の強固な基盤です。

4. UserRepository

次に、UserRepositoryを見てみましょう。

JpaSpecificationExecutor を拡張して、新しい仕様APIを取得します。

public interface UserRepository 
  extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {}

5. 検索クエリをテストする

それでは、新しい検索APIをテストしてみましょう。

まず、テストの実行時にユーザーを準備するために、いくつかのユーザーを作成しましょう。

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

次に、名前がのユーザーを見つける方法を見てみましょう。

@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = 
      new UserSpecification(new SearchCriteria("lastName", ":", "doe"));
    
    List<User> results = repository.findAll(spec);

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

これで、名前と名前の両方が指定されたのユーザーが見つかります。

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec1 = 
      new UserSpecification(new SearchCriteria("firstName", ":", "john"));
    UserSpecification spec2 = 
      new UserSpecification(new SearchCriteria("lastName", ":", "doe"));
    
    List<User> results = repository.findAll(Specification.where(spec1).and(spec2));

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

注:ここでを使用して仕様を組み合わせました。

次に、名前と最低年齢の両方が指定されているユーザーを見つけましょう。

@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec1 = 
      new UserSpecification(new SearchCriteria("age", ">", "25"));
    UserSpecification spec2 = 
      new UserSpecification(new SearchCriteria("lastName", ":", "doe"));

    List<User> results = 
      repository.findAll(Specification.where(spec1).and(spec2));

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

次に、が実際には存在しないユーザーを検索する方法を説明します

@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec1 = 
      new UserSpecification(new SearchCriteria("firstName", ":", "Adam"));
    UserSpecification spec2 = 
      new UserSpecification(new SearchCriteria("lastName", ":", "Fox"));

    List<User> results = 
      repository.findAll(Specification.where(spec1).and(spec2));

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

最後に、名の一部のみが指定された Userが見つかります。

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

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

6. 仕様を組み合わせる

次に、カスタム仕様を組み合わせて、複数の制約を使用し、複数の基準に従ってフィルタリングする方法を見てみましょう。

ビルダー— UserSpecificationsBuilder —を実装して、Specificationsを簡単かつ流暢に組み合わせることができます。

public class UserSpecificationsBuilder {
    
    private final List<SearchCriteria> params;

    public UserSpecificationsBuilder() {
        params = new ArrayList<SearchCriteria>();
    }

    public UserSpecificationsBuilder with(String key, String operation, Object value) {
        params.add(new SearchCriteria(key, operation, value));
        return this;
    }

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

        List<Specification> 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

最後に、この新しい永続性検索/フィルター機能を使用し、単純なsearch操作でUserControllerを作成して、 RESTAPIを設定しましょう。

@Controller
public class UserController {

    @Autowired
    private UserRepository repo;

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

他の英語以外のシステムをサポートするために、Patternオブジェクトを変更できることに注意してください。

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

APIをテストするためのテストURLは次のとおりです。

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

そして、これが応答です:

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

パターンの例では、検索が「、」で分割されているため、検索語にこの文字を含めることはできません。パターンも空白と一致しません。

カンマを含む値を検索する場合は、「;」などの別の区切り文字の使用を検討できます。

別のオプションは、引用符の間の値を検索するようにパターンを変更してから、検索語からこれらを削除することです。

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

8. 結論

この記事では、強力なRESTクエリ言語のベースとなる簡単な実装について説明しました。

Spring Data仕様をうまく利用して、APIをドメインから遠ざけ、には他の多くの種類の操作を処理するオプションがあります。

この記事の完全な実装は、GitHubプロジェクトにあります。 これはMavenベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。