1. 개요
이 사용방법(예제)에서는 Spring Data JPA 및 Querydsl 을 사용하여 REST API 용 쿼리 언어를 빌드하는 방법을 살펴봅니다 .
이 시리즈 의 처음 두 기사에서는 JPA Criteria 및 Spring Data 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 엔터티
다음 – 검색 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 를 생성해 보겠습니다.
여기서는 자동으로 생성된 Q 유형 대신 PathBuilder 를 사용 하고 있습니다. 보다 추상적인 사용을 위해 동적으로 경로를 생성해야 하기 때문입니다.
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 : 연산 – 예: Equality, less than, ... 등
- value : 필드 값 – 예: john, 25, … 등
5. 내 사용자 저장소
이제 MyUserRepository 를 살펴 보겠습니다 .
나중에 조건 자를 사용 하여 검색 결과를 필터링 할 수 있도록 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. 사용자 컨트롤러
마지막으로 모든 것을 모아 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 기반 프로젝트이므로 그대로 가져오고 실행하기 쉬워야 합니다.