Spring Data JPAとQuerydslを使ったRESTクエリ言語

Spring Data JPAおよびQuerydslを使用したRESTクエリ言語

1. 概要

このチュートリアルでは、REST API using Spring Data JPA and Querydslのクエリ言語の構築について説明します。

this seriesの最初の2つの記事では、JPACriteriaとSpringDataJPA仕様を使用して同じ検索/フィルタリング機能を構築しました。

つまり–why a query language?なぜなら–十分に複雑なAPIの場合–非常に単純なフィールドでリソースを検索/フィルタリングするだけでは不十分です。 A query language is more flexibleであり、必要なリソースに正確に絞り込むことができます。

2. Querydsl構成

まず、Querydslを使用するようにプロジェクトを構成する方法を見てみましょう。

次の依存関係をpom.xmlに追加する必要があります。


    com.querydsl
    querydsl-apt
    4.1.4
    

    com.querydsl
    querydsl-jpa
    4.1.4

また、次のようにAPT –注釈処理ツール–プラグインを構成する必要があります。


    com.mysema.maven
    apt-maven-plugin
    1.1.3
    
        
            
                process
            
            
                target/generated-sources/java
                com.mysema.query.apt.jpa.JPAAnnotationProcessor
            
        
    

3. MyUserエンティティ

次へ– Search APIで使用する「MyUser」エンティティを見てみましょう。

@Entity
public class MyUser {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;
    private String email;

    private int age;
}

4. PathBuilderを使用したカスタムPredicate

それでは、任意の制約に基づいてカスタムPredicateを作成しましょう。

ここでは、自動生成されたQタイプの代わりにPathBuilderを使用しています。これは、より抽象的な使用法のためにパスを動的に作成する必要があるためです。

public class MyUserPredicate {

    private SearchCriteria criteria;

    public BooleanExpression getPredicate() {
        PathBuilder entityPath = new PathBuilder<>(MyUser.class, "user");

        if (isNumeric(criteria.getValue().toString())) {
            NumberPath path = entityPath.getNumber(criteria.getKey(), Integer.class);
            int value = Integer.parseInt(criteria.getValue().toString());
            switch (criteria.getOperation()) {
                case ":":
                    return path.eq(value);
                case ">":
                    return path.goe(value);
                case "<":
                    return path.loe(value);
            }
        }
        else {
            StringPath path = entityPath.getString(criteria.getKey());
            if (criteria.getOperation().equalsIgnoreCase(":")) {
                return path.containsIgnoreCase(criteria.getValue().toString());
            }
        }
        return null;
    }
}

述語の実装がgenerically dealing with multiple types of operationsであることに注意してください。 これは、クエリ言語が定義上、サポートされている操作を使用して任意のフィールドでフィルタリングできる可能性があるオープン言語であるためです。

この種のオープンフィルタリング基準を表すために、シンプルですが非常に柔軟な実装を使用しています–SearchCriteria

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

SearchCriteriaは、制約を表すために必要な詳細を保持します。

  • key:フィールド名–例:firstNameage、…など

  • operation:演算–例:等式、より小さい、…など

  • value:フィールド値–例:john、25、…など

5. MyUserRepository

では、MyUserRepositoryを見てみましょう。

後でPredicatesを使用して検索結果をフィルタリングできるように、QuerydslPredicateExecutorを拡張するためにMyUserRepositoryが必要です。

public interface MyUserRepository extends JpaRepository,
  QuerydslPredicateExecutor, QuerydslBinderCustomizer {
    @Override
    default public void customize(
      QuerydslBindings bindings, QMyUser root) {
        bindings.bind(String.class)
          .first((SingleValueBinding) StringExpression::containsIgnoreCase);
        bindings.excluding(root.email);
      }
}

6. Predicatesを組み合わせる

次に、結果のフィルタリングで複数の制約を使用するための述語の組み合わせを見てみましょう。

次の例では、ビルダー–MyUserPredicatesBuilder –を使用して、Predicatesを結合します。

public class MyUserPredicatesBuilder {
    private List params;

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

    public MyUserPredicatesBuilder with(
      String key, String operation, Object value) {

        params.add(new SearchCriteria(key, operation, value));
        return this;
    }

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

        List predicates = params.stream().map(param -> {
            MyUserPredicate predicate = new MyUserPredicate(param);
            return predicate.getPredicate();
        }).filter(Objects::nonNull).collect(Collectors.toList());

        BooleanExpression result = Expressions.asBoolean(true).isTrue();
        for (BooleanExpression predicate : predicates) {
            result = result.and(predicate);
        }
        return result;
    }
}

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

次に、Search APIをテストしましょう。

まず、数人のユーザーでデータベースを初期化し、これらをテストできるように準備します。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceConfig.class })
@Transactional
@Rollback
public class JPAQuerydslIntegrationTest {

    @Autowired
    private MyUserRepository repo;

    private MyUser userJohn;
    private MyUser userTom;

    @Before
    public void init() {
        userJohn = new MyUser();
        userJohn.setFirstName("John");
        userJohn.setLastName("Doe");
        userJohn.setEmail("[email protected]");
        userJohn.setAge(22);
        repo.save(userJohn);

        userTom = new MyUser();
        userTom.setFirstName("Tom");
        userTom.setLastName("Doe");
        userTom.setEmail("[email protected]");
        userTom.setAge(26);
        repo.save(userTom);
    }
}

次に、given last nameのユーザーを見つける方法を見てみましょう。

@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("lastName", ":", "Doe");

    Iterable results = repo.findAll(builder.build());
    assertThat(results, containsInAnyOrder(userJohn, userTom));
}

次に、指定されたboth first and last nameを持つユーザーを見つける方法を見てみましょう。

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("firstName", ":", "John").with("lastName", ":", "Doe");

    Iterable results = repo.findAll(builder.build());

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

次に、指定されたboth last name and minimum ageを持つユーザーを見つける方法を見てみましょう

@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("lastName", ":", "Doe").with("age", ">", "25");

    Iterable results = repo.findAll(builder.build());

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

それでは、doesn’t actually existであるMyUserを検索する方法を見てみましょう。

@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("firstName", ":", "Adam").with("lastName", ":", "Fox");

    Iterable results = repo.findAll(builder.build());
    assertThat(results, emptyIterable());
}

最後に、次の例のように、MyUsergiven only part of the first nameを見つける方法を見てみましょう。

@Test
public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("firstName", ":", "jo");

    Iterable results = repo.findAll(builder.build());

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

8. UserController

最後に、すべてをまとめてRESTAPIを構築しましょう。

クエリ文字列で渡す「search」パラメータを使用して単純なメソッドfindAll()を定義するUserControllerを定義しています。

@Controller
public class UserController {

    @Autowired
    private MyUserRepository myUserRepository;

    @RequestMapping(method = RequestMethod.GET, value = "/myusers")
    @ResponseBody
    public Iterable search(@RequestParam(value = "search") String search) {
        MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder();

        if (search != null) {
            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));
            }
        }
        BooleanExpression exp = builder.build();
        return myUserRepository.findAll(exp);
    }
}

以下に簡単なテストURLの例を示します。

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

そして応答:

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

9. 結論

この3番目の記事では、Querydslライブラリをうまく利用してthe first steps of building a query language for a REST APIを取り上げました。

実装はもちろん早い段階ですが、追加の操作をサポートするために簡単に進化させることができます。

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