스프링 데이터 JPA-09.쿼리 만들기 개요 및 실습,비동기 쿼리(스프링 데이터 Common)
스프링 데이터 Common: 쿼리 만들기 개요
스프링 데이터 저장소의 메소드 이름으로 쿼리 만드는 방법
- 메소드 이름을 분석해서 쿼리 만들기 (CREATE)
- 미리 정의해 둔 쿼리 찾아 사용하기 (USE_DECLARED_QUERY)
- 미리 정의한 쿼리 찾아보고 없으면 만들기 (CREATE_IF_NOT_FOUND)
@SpringBootApplication
@EnableJpaRepositories(queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND) // 기본값
//@EnableJpaRepositories(queryLookupStrategy = QueryLookupStrategy.Key.CREATE)
//@EnableJpaRepositories(queryLookupStrategy = QueryLookupStrategy.Key.USE_DECLARED_QUERY)
public class JpaApplication {
public static void main(String[] args) {
SpringApplication.run(JpaApplication.class, args);
}
}
쿼리 만드는 방법
리턴타입 {접두어}{도입부}By{프로퍼티 표현식}(조건식)[(And Or){프로퍼티 표현식}(조건식)]{정렬 조건} (매개변수)
구분 | 내용 |
---|---|
접두어 | Find, Get, Query, Count, … |
도입부 | Distinct, First(N), Top(N) |
프로퍼티 표현식 | Person.Address.ZipCode => find(Person)ByAddress_ZipCode(…) |
조건식 | IgnoreCase, Between, LessThan, GreaterThan, Like, Contains, … |
정렬 조건 | OrderBy{프로퍼티}Asc | Desc |
리턴 타입 | E, Optional |
매개변수 | Pageable, Sort |
쿼리 찾는 방법
- 메소드 이름으로 쿼리를 표현하기 힘든 경우에 사용.
- 저장소 기술에 따라 다름.
- JPA: @Query @NamedQuery
스프링 데이터 Common: 쿼리 만들기 실습
기본 예제
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// distinct
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// ignoring case
List<Person> findByLastnameIgnoreCase(String lastname);
// ignoring case
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
정렬
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
페이징
Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
스트리밍
Stream
- try-with-resource 사용할 것. (Stream을 다 쓴다음에 close() 해야 함)
public interface CommentRepository extends MyRepository<Comment, Long> {
List<Comment> findByCommentContains(String keyword);
List<Comment> findByCommentContainsAndLikeCountAfter(String comment, int likeCount);
List<Comment> findByCommentContainsAndLikeCountGreaterThanEqual(String comment, int likeCount);
List<Comment> findByCommentContainsIgnoreCaseOrderByLikeCountDesc(String comment);
Page<Comment> findByCommentContainsIgnoreCase(String comment, Pageable pageable);
Stream<Comment> findByCommentContainsOrderByLikeCountDesc(String comment, Pageable pageable);
@Test
public void crud() throws ExecutionException, InterruptedException {
createComment("spring data jpa", 30);
createComment("spring web mvc", 40);
createComment("spring rest api", 50);
// spring 포함
List<Comment> list = commentRepository.findByCommentContains("spring");
assertEquals(list.size(), 3);
// 40 초과
list = commentRepository.findByCommentContainsAndLikeCountAfter("spring", 40);
assertEquals(list.size(), 1);
// 40 이상
list = commentRepository.findByCommentContainsAndLikeCountGreaterThanEqual("spring", 40);
assertEquals(list.size(), 2);
// 정렬 및 대소문자(IgnoreCase)
list = commentRepository.findByCommentContainsIgnoreCaseOrderByLikeCountDesc("SprinG");
assertEquals(list.size(), 3);
assertEquals(list.get(0).getLikeCount(), 50);
// 페이징
PageRequest pageRequest = PageRequest.of(0, 10, Sort.Direction.DESC, "LikeCount");
Page<Comment> pageList = commentRepository.findByCommentContainsIgnoreCase("spring", pageRequest);
assertEquals(pageList.getNumberOfElements(), 3);
assertEquals(pageList.getTotalPages(), 1);
// stream
try(Stream<Comment> comments = commentRepository.findByCommentContainsOrderByLikeCountDesc("spring", pageRequest)){
Comment firstComment = comments.findFirst().get();
assertEquals(firstComment.getLikeCount(), 50);
}
}
스프링 데이터 Common: 비동기 쿼리
비동기 쿼리
- @Async Future
findByFirstname(String firstname); - @Async CompletableFuture
findOneByFirstname(String firstname); - @Async ListenableFuture
findOneByLastname(String lastname); - 해당 메소드를 스프링 TaskExecutor에 전달해서 별도의 쓰레드에서 실행함.
- Reactive랑은 다른 것임
권장하지 않는 이유
- 테스트 코드 작성이 어려움.
- 코드 복잡도 증가.
- 성능상 이득이 없음.
- DB 부하는 결국 같고.
- 메인 쓰레드 대신 백드라운드 쓰레드가 일하는 정도의 차이.
- 단, 백그라운드로 실행하고 결과를 받을 필요가 없는 작업이라면 @Async를 사용해서 응답 속도를 향상 시킬 수는 있다.
@Async
//Future<List<Comment>> findByCommentContains(String comment, Pageable pageable);
ListenableFuture<List<Comment>> findByCommentContains(String comment, Pageable pageable);
// 권장하지 않음, 사용하려면 애플리케이션에 @EnableAsync 붙여줘야한다.
// 이 코드가 다른 스레드로 떨어져 있다고 생각.
// 기존 데이터는 select 가능하나, 여기서 지금 조작중인 데이터는 감지하지 못한다.
// Future<List<Comment>> future = commentRepository.findByCommentContains("spring", pageRequest);
ListenableFuture<List<Comment>> future = commentRepository.findByCommentContains("spring", pageRequest);
System.out.println("==============");
System.out.println("is done? " + future.isDone());
future.addCallback(new ListenableFutureCallback<List<Comment>>() {
@Override
public void onFailure(Throwable throwable) {
System.out.println("fail");
System.out.println(throwable);
}
@Override
public void onSuccess(@Nullable List<Comment> comments) {
System.out.println("onSuccess");
System.out.println(comments.size()); // 0 만 조회됨.
// 이 트랜잭선 이전의 데이터 조회. 변화 감지 못함 (위에서 insert 한 것 감지못함)
}
});
List<Comment> futureList = future.get();
futureList.forEach(System.out::println); // 출력안됨
}
쉽게 다시 설명하자면,
선언한 비동기 쿼리는 다른 스레드
즉, 지금 저 테스트에서 도는 메인 스레드와 별개로 다른 스레드가 처리한다.
이 다른 스레드가 비동기 쿼리를 처리할 때
- 단순히 기존에 존재하던 데이터의 조회를 하는 것이라면 상관이 없다.
- 그러나 현재 메인 스레드에서 데이터를 조작한다고 했을때, 이 변화를 감지하지 못한다.
사용한 테스트 코드를 참조하면
- 시작점에서 데이터를 3건 insert 해주지만, 이걸 감지하지 못하고 0건만 나온다.
- 왜냐햐면 비동기 쿼리(저기 해당하는 코드)는 메소드 시작부터 이미 다른 곳에 가 있다고 생각하고, 처음부터 다른 스레드가 처리한다고 생각하면 편하다.
- 시점이 저 메서드를 시작하기 이전으로 봐야 한다. (저 메소드가 시작하기 전에는 데이터가 없었다. 그래서 0건 출력됨)