DATA JPA의 메서드 이름으로 쿼리를 생성, @Query로 생성, named queries? 로 정적 쿼리를 생성할 수 있다. 하지만 동적쿼리는 생성 불가하다. (동적쿼리 : 동일한 수의 조건문들이 없다.) Querydsl 혹은 Criteria API를 사용할 수 있다.
Querydsl 장점
동적 쿼리 지원, 매우 간결한 API -> 복잡한 쿼리를 생성하기 쉽고 읽기 쉽다.
JDO,Lucene,MongoDB를 지원한다.
QueryDsl은 Type-Safe한 쿼리를 위한 스프링에서 제공하는 Domain Specific Language입니다.
SQL같이 문자로 Type Check가 불가능하고 실행하기 전까지 작동 여부를 확인 하기 어려운 부분을 보완하여 SQL을 Java로 Type-Safe하게 개발 할 수 있게 해주는 프레임워크입니다.
Repository 인터페이스에서 메서드명이 너무 길어지거나 메서드명으로 쿼리문을 만들기 까다로운 조건일 때 유용하게 쓰일 수 있습니다.
QuerydslPredicateExecutor는 이 Repository가 QueryDsl을 실행할 수 있는 인터페이스를 제공하는 역할을 합니다.
@DataJpaTest
public class AccountRepositoryTest {
@Autowired
AccountRepository accountRepository;
@Test
public void crud() {
Account account = new Account();
account.setFirstName("saelobi");
account.setLastName("kimbs");
accountRepository.save(account);
Predicate predicate = QAccount.account
.firstName.containsIgnoreCase("saelobi")
.and(QAccount.account.lastName.startsWith("kim"));
Optional<Account> one = accountRepository.findOne(predicate);
assertThat(one.orElseThrow(() -> new EntityNotFoundException())
.getFirstName()).isEqualTo("saelobi");
assertThat(one.orElseThrow(() -> new EntityNotFoundException())
.getFirstName()).isEqualTo("saelobi");
}
}
위와 같이 accountRepository를 통해서 QueryDsl을 사용할 수 있습니다. Repository 인터페이스에서 메서드명으로 쿼리를 만드는 방식에 비해 가독성이 좋고 또한 Type-Safe하기 때문에 컴파일 타임때 오타 등 어떤 문제가 발생했을 시 미리 알려줄 수 있으므로 버그를 추적하기에도 용이합니다.
?firstName.containsIgnoreCase
?firstName은 StringPath클래스?
We can create database queries with Querydsl by following these steps:
1.Modify the repository interface to support queries that use Querydsl.
2.Specify the conditions of the invoked database query.
3.Invoke the database query.
QueryDslPredicateExecutor에 Querydsl을 사용하는 db query를 호출하는데 쓰이는 메서드가 선언돼있다.
QueryDslPredicateExecutor에 타입 매개변수 T는 쿼리에 쓰이는 Entity의 타입을 나타낸다.
Querydsl을 사용해 db query를 지원하기 위해 QueryDslPredicateExecutor를 Repository에 상속시키고, T에 queried entity의 타입을 지정한다.
QueryDslPredicateExecutor을 상속받은 repository는 다음 메서드에 접근을 갖는다.
long count(Predicate predictae)
boolean exists(Predicate predicate)
Iterable<T> findAll(Predicate predicate)
T findOne(Predicate predicate)
QueryDslPredicateExecutor는 Predicate 객체에 명시된 조건을 만족하기 위해 objects를 sort와 paginate하는 methods들도 선언돼있다.
query 결과를 정렬하고 페이지네이션 한다.
생성된 Q type class로 db query의 조건을 명시할 수 있다. 다음 단계로 한다.
1. query에 사용되는 Entity를 나타내는 query 객체의 참조를 얻는다.
2. db query의 조건을 명시하는 Predicate 객체를 생성한다.
public class QTodo extends EntityPathBase<Todo> {
public static final QTodo todo = new QTodo("todo");
}
QTodo todo = QTodo.todo;
single field의 값을 제한하는 조건문을 생성할 수 있다. 다음 단계를 통해
1. 조건문의 target field를 선택한다.
2. 조건문을 명시한다.
title이 'Foo'인 Todo 객체를 반환하는 Predicate object를 생성하길 원한다면 다음 코드를 통해 생성할 수 있다.
Predicate titleIs = QTodo.todo.title.eq("Foo");
BooleanExpression클래스를 이용해 Predicate objects를 결합할 수 있다.
ex1)
If we want to select todo entries whose title is 'Foo' and description is 'Bar', we can create the Predicate object by using the following code:
Predicate titleAndDescriptionAre = QTodo.todo.title.eq("Foo") .and(QTodo.todo.description.eq("Bar"));
EX2)
If we want select todo entries whose title is 'Foo' or 'Bar', we can create the Predicate object by using the following code:
Predicate titleIs = QTodo.todo.title.eq("Foo") .or(QTodo.todo.title.eq("Bar");
EX3)
If we want to select todo entries whose title is 'Foo' and description is not 'Bar', we can create the Predicate object by using the following code:
Predicate titleIsAndDescriptionIsNot = QTodo.todo.title.eq("Foo") .and(QTodo.todo.description.eq("Bar").not());
I argue that we should create these objects by using predicate builder classes because this way we will put our query generation logic into one place.
We can create a predicate builder class by following these steps:
1. I argue that we should create these objects by using predicate builder classes because this way we will put our query generation logic into one place.
2.Add a private constructor the created class. This ensures that no one can instantiate our predicate builder class.
3.Add static predicate builder methods to this class. In our case, we will add only one predicate builder method (hasTitle(String title)) to this class and implement it by returning a new Predicate object.
``
import com.mysema.query.types.Predicate;
final class TodoPredicates {
private TodoPredicates() {}
static Predicate hasTitle(String title) {
return QTodo.todo.title.eq(title);
``
After we have specified the conditions of the invoked query by creating a new Predicate object, we can invoke the database query by using the methods provided by the QueryDslPredicateExecutor interface.
Example 1:
If we want to get the number of Todo objects that have the title 'foo', we have to create and invoke our database query by using this code:
Predicate pred = TodoPredicates.hasTitle("foo"); long count = repository.count(pred);
Example 2:
If we want to know if the database contains Todo objects that have the title 'foo', we have to create and invoke our database query by using this code:
Predicate pred = TodoPredicates.hasTitle("foo"); boolean exists = repository.exists(pred);
Example 3:
If we want to the get all Todo objects that have the title 'foo', we have to create and invoke our database query by using this code:
Predicate pred = TodoPredicates.hasTitle("foo"); Iterable<Todo> todoEntries = repository.findAll(pred);
Example 4:
If we want to get the Todo object whose title is 'foo', we have to create and invoke our database query by using this code:
Predicate pred = TodoPredicates.hasTitle("foo"); Todo todoEntry = repository.findOne(pred);
We can implement our search function by following these steps:
1.Modify our repository interface to support Querydsl.
2.Create the predicate builder class that creates Predicate objects.
3.Implement the service method that uses our predicate builder class and invokes the created database query by using our repository interface.
We can create the predicate builder class that fulfils the requirements of our search function by following these steps:
1.Create the predicate builder class and ensure that it cannot be instantiated.
2.Add a static titleOrDescriptionContainsIgnoreCase(String searchTerm) method to the predicate builder class and set its return type to Predicate.
3.Implement the titleOrDescriptionContainsIgnoreCase(String searchTerm) method by following these rules:
If the searchTerm is null or empty, return a Predicate object that returns all todo entries.
If the searchTerm is not null, return a Predicate object that ignores case and returns todo entries whose title or description contains the given search term.
The source code of our predicate builder class looks as follows:
import com.mysema.query.types.Predicate;
``
final class TodoPredicates {
private TodoPredicates() {}
static Predicate titleOrDescriptionContainsIgnoreCase(String searchTerm) {
if (searchTerm == null || searchTerm.isEmpty()) {
return QTodo.todo.isNotNull();
}
else {
return QTodo.todo.description.containsIgnoreCase(searchTerm)
.or(QTodo.todo.title.containsIgnoreCase(searchTerm));
}
}
}
``
The first thing that we have to do is to create an interface called TodoSearchService. This interface declares one method called findBySearchTerm(). This method takes the search term as a method parameter and returns a list of TodoDTO objects. The source code of the TodoSearchService interface looks as follows:
``
import java.util.List;
public interface TodoSearchService {
List<TodoDTO> findBySearchTerm(String searchTerm);
}
``
1. search interface를 상속받는 @Service를 붙인 Service 구현체 생성한다.
2. private final 인 TodoRepository 필드를 더한다.
3. 생성자 주입으로 필드를 주입한다.
4. search interface의 메서드를 override한다. @Transactional(readOnly=true)를 붙인다.
5. search method는
1. Predicate Builder의 메서드를 호출하여 Predicate 객체를 얻는다.
2. repository 필드의 메서드(QuerydslPredicateExecutor에 정의된 메서드 findAll(Predicate p) )에 Predicate 객체를 파라미터로 전달하여 호출한다.
3. 반환된 Iterable<>entity 객체를 List Dto객체로 변환하여 반환한다.
import static net.petrikainulainen.springdata.jpa.todo.TodoPredicates.titleOrDescriptionContainsIgnoreCase;
@Service
final class RepositoryTodoSearchService implements TodoSearchService {
private final TodoRepository repository;
@Autowired
public RepositoryTodoSearchService(TodoRepository repository) {
this.repository = repository;
}
@Transactional(readOnly = true)
@Override
public List<TodoDTO> findBySearchTerm(String searchTerm) {
Predicate searchPred = titleOrDescriptionContainsIgnoreCase(searchTerm);
Iterable<Todo> searchResults = repository.findAll(searchPred);
return TodoMapper.mapEntitiesIntoDTOs(searchResults);
}
}
요약
Querydsl설정하고 빌드하여 Qclass를 생성한다.
Querydsl쓰는 query생성하고 싶다면 , QuerydslPredicateExecutor<> 을 상속시킨다.
Predicate Builder 클래스 생성하여 Predicate 객체를 생성한다.
QuerydslPredicateExecutor<> 의 메서드에 Predicate 객체를 파라미터로 전달하여 호출.
queried Entities의 Iterable 객체를 List로 변환한다.
동적쿼리를 써야할 떄는 Querydsl을 써야한다.