1. 개요

이 예제에서는 SpringData JPA 및 Querydsl을 사용하여 REST API 용 쿼리 언어를 빌드하는 방법 살펴 봅니다 .

이 시리즈 의 처음 두 기사에서는 JPA Criteria 및 SpringData JPA 사양을 사용하여 동일한 검색 / 필터링 기능을 구축했습니다.

그래서 – 왜 쿼리 언어입니까? 왜냐하면 – 충분히 복잡한 API의 경우 – 매우 간단한 필드로 리소스를 검색 / 필터링하는 것만으로는 충분하지 않습니다. 쿼리 언어는 더 유연 하며 필요한 리소스로 정확히 필터링 할 수 있습니다.

2. Querydsl 구성

먼저 Querydsl을 사용하도록 프로젝트를 구성하는 방법을 살펴 보겠습니다.

pom.xml에 다음 의존성을 추가해야합니다 .

<dependency> 
    <groupId>com.querydsl</groupId> 
    <artifactId>querydsl-apt</artifactId> 
    <version>4.2.2</version>
    </dependency>
<dependency> 
    <groupId>com.querydsl</groupId> 
    <artifactId>querydsl-jpa</artifactId> 
    <version>4.2.2</version> 
</dependency>

또한 APT – 어노테이션 처리 도구 – 플러그인을 다음과 같이 구성해야합니다.

<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>1.1.3</version>
    <executions>
        <execution>
            <goals>
                <goal>process</goal>
            </goals>
            <configuration>
                <outputDirectory>target/generated-sources/java</outputDirectory>
                <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>

그러면 엔티티에 대한 Q 유형 이 생성 됩니다. 

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. 정의 술어 W 번째 PathBuilder

이제 랜덤의 제약 조건을 기반으로 사용자 지정 Predicate생성 해 보겠습니다 .

우리가 사용하고있는 PathBuilder을 우리가 더 추상적 사용에 대한 동적 경로를 만들 필요가 있기 때문에 여기에 대신 자동으로 생성 된 Q-유형 :

public class MyUserPredicate {

    private SearchCriteria criteria;

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

        if (isNumeric(criteria.getValue().toString())) {
            NumberPath<Integer> 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;
    }
}

술어의 구현이 일반적으로 여러 유형의 작업을 처리 하는 방법에 유의하십시오 . 이는 쿼리 언어가 정의에 따라 지원되는 모든 작업을 사용하여 필드별로 필터링 할 수있는 개방형 언어이기 때문입니다.

이러한 종류의 개방형 필터링 기준을 나타 내기 위해 간단하지만 매우 유연한 구현 인 SearchCriteria를 사용하고 있습니다 .

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

SearchCriteria은 우리가 제약 조건을 표현하기 위해 필요한 세부 사항을 원하는 분야

  • key : 필드 이름 – 예 : firstName , age ,… 등
  • operation : 작업 – 예 : 같음,보다 작음,… 등
  • value : 필드 값 – 예 : john, 25,… 등

5. MyUserRepository

이제 MyUserRepository를 살펴 보겠습니다 .

나중에 Predicates 를 사용 하여 검색 결과를 필터링 할 수 있도록 QuerydslPredicateExecutor 를 확장 하려면 MyUserRepository필요 합니다.

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

여기서는 MyUser 엔터티에 대해 생성 된 Q- 유형을 사용하고 있으며 이름은 QMyUser입니다.

6. 술어 결합

다음으로 결과 필터링에서 여러 제약 조건을 사용하기 위해 술어를 결합하는 방법을 살펴 보겠습니다.

다음 예에서는 빌더 ( MyUserPredicatesBuilder) 를 사용하여 Predicates 를 결합합니다 .

public class MyUserPredicatesBuilder {
    private List<SearchCriteria> 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. 검색 쿼리 테스트

다음으로 검색 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("john@doe.com");
        userJohn.setAge(22);
        repo.save(userJohn);

        userTom = new MyUser();
        userTom.setFirstName("Tom");
        userTom.setLastName("Doe");
        userTom.setEmail("tom@doe.com");
        userTom.setAge(26);
        repo.save(userTom);
    }
}

다음으로 성이 지정된 사용자를 찾는 방법을 살펴 보겠습니다 .

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

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

이제 이름과 성을 모두 지정한 사용자를 찾는 방법을 살펴 보겠습니다 .

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

    Iterable<MyUser> results = repo.findAll(builder.build());

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

다음 으로 성 및 최소 연령을 모두 지정한 사용자를 찾는 방법을 살펴 보겠습니다.

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

    Iterable<MyUser> results = repo.findAll(builder.build());

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

자,하자가를 검색하는 방법을 참조 참고 MyUser 실제로 존재하지 않습니다 :

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

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

마지막으로 다음 예에서와 같이 이름의 일부만 제공된 MyUser 를 찾는 방법을 살펴 보겠습니다.

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

    Iterable<MyUser> results = repo.findAll(builder.build());

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

8. UserController

마지막으로 모든 것을 통합하고 REST API를 구축해 보겠습니다.

쿼리 문자열을 전달할 " search "매개 변수를 사용 하여 간단한 메서드 findAll ()정의하는 UserController 를 정의 하고 있습니다.

@Controller
public class UserController {

    @Autowired
    private MyUserRepository myUserRepository;

    @RequestMapping(method = RequestMethod.GET, value = "/myusers")
    @ResponseBody
    public Iterable<MyUser> 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":"tom@doe.com",
    "age":26
}]

9. 결론

이 세 번째 기사에서는 Querydsl 라이브러리를 잘 활용 하여 REST API 용 쿼리 언어를 빌드하는 첫 번째 단계에 대해 설명했습니다 .

물론 구현은 초기 단계이지만 추가 작업을 지원하도록 쉽게 발전시킬 수 있습니다.

이 기사 전체 구현GitHub 프로젝트 에서 찾을 수 있습니다. 프로젝트 는 Maven 기반 프로젝트이므로 그대로 가져 와서 실행하기 쉽습니다.

다음 »
REST 쿼리 언어 – 고급 검색 작업
« 이전
SpringData JPA 사양을 사용한 REST 쿼리 언어