티스토리 뷰
Derived Query
스프링 JPA 는 메소드 이름으로 쿼리를 생성하는 기능을 제공한다. 이를 파생된 쿼리라고 하며 엔터티 속성들을 다양하게 활용하여 쿼리를 생성해 낼 수 있다.
이번 아티클에서는 이름을 통한 파생쿼리를 이용하여 우리가 원하는 쿼리를 어떻게 작성할 수 있는지 살펴 볼 것이다.
Query Sample 살펴보기.
아래는 JPA 가이드 샘플 내용이다. Sample
Distinct
findDistinctByLastnameAndFirstname
lastname, firstname과 동일한 결과를 찾아서 중복제거(distinct)를 수행한다.
select distinct … where x.lastname = ?1 and x.firstname = ?2
And
findByLastnameAndFirstname
Where 절에 들어갈 criteria 를 and 로 연결한다.
… where x.lastname = ?1 and x.firstname = ?2
Or
findByLastnameOrFirstname
Where 절에 들어갈 criteria 를 or 로 연결한다.
… where x.lastname = ?1 or x.firstname = ?2
Is, Equals
findByFirstname,findByFirstnameIs,findByFirstnameEquals
exact 매칭을 수행한다.
… where x.firstname = ?1
Between
findByStartDateBetween
between 절을 이용하여 조회한다.
… where x.startDate between ?1 and ?2
LessThan
findByAgeLessThan
… where x.age < ?1
LessThanEqual
findByAgeLessThanEqual
… where x.age <= ?1
GreaterThan
findByAgeGreaterThan
… where x.age > ?1
GreaterThanEqual
findByAgeGreaterThanEqual
… where x.age >= ?1
After
findByStartDateAfter
… where x.startDate > ?1
Before
findByStartDateBefore
… where x.startDate < ?1
IsNull, Null
findByAge(Is)Null
Age가 NULL 인 데이터를 조회한다.
… where x.age is null
IsNotNull, NotNull
findByAge(Is)NotNull
Age가 NULL 이 아닌 데이터를 조회한다.
… where x.age not null
Like
findByFirstnameLike
… where x.firstname like ?1
NotLike
findByFirstnameNotLike
… where x.firstname not like ?1
StartingWith
findByFirstnameStartingWith
… where x.firstname like ?1 (parameter bound with appended %)
EndingWith
findByFirstnameEndingWith
… where x.firstname like ?1 (parameter bound with prepended %)
Containing
findByFirstnameContaining
… where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy
findByAgeOrderByLastnameDesc
… where x.age = ?1 order by x.lastname desc
Not
findByLastnameNot
… where x.lastname <> ?1
In
findByAgeIn(Collection ages)
… where x.age in ?1
NotIn
findByAgeNotIn(Collection ages)
… where x.age not in ?1
True
findByActiveTrue()
… where x.active = true
False
findByActiveFalse()
… where x.active = false
IgnoreCase
findByFirstnameIgnoreCase
… where UPPER(x.firstname) = UPPER(?1)
위 내용을 바탕으로 지금까지 우리가 만들어둔 데이터를 조회해 보자.
Query 수행하기.
회원 이름이 ‘kido’ 이고 데이터 생성일이 10일전부터 오늘 사이의 회원을 조회하기.
List findAllByNameAndCreatedAtBetween(String name, LocalDateTime startDate, LocalDateTime endDate);
- findAll: 모두 조회하라.
- By: 다음부터 나오는 조건을 비교하라.
- ByName: 이름이 매칭되는 즉, name = ? 으로 해석된다.
- And: And 조건
- CreatedAtBetween: createdAt 에 대해서 between 조건을 검사하라.
위 생성 쿼리 결과는 다음과 같다.
Hibernate:
select
user0_.id as id1_5_,
user0_.birth as birth2_5_,
user0_.created_at as created_3_5_,
user0_.name as name4_5_
from
user user0_
where
user0_.name=?
and (
user0_.created_at between ? and ?
)
우리가 의도한 대로 조회가 되는 것을 확인할 수 있다.
Top, First 쿼리
특정 조회 결과에서 1명만 조회하라 혹은 N명 조회하라는 요구사항이 일어날 수 있다. 이때는 Top 혹은 First 를 활용하면 쉽게 해결이 된다.
List findTop3ByName(String name);
- Top{N}: Top 만 수행하면 1개의 결과만 반환한다. 그러나 N을 지정해주면 지정된 수만큼 가져온다.
- First 와도 동일하다.
Hibernate:
select
user0_.id as id1_5_,
user0_.birth as birth2_5_,
user0_.created_at as created_3_5_,
user0_.name as name4_5_
from
user user0_
where
user0_.name=? limit ?
쿼리 결과가 Limit 된것을 확인할 수 있다.
Sorting
사용자 이름으로 조회를 하는데, 사용자 정보가 생성된 일자를 기준으로 소트한다고 해보자.
이때 Repository 에는 다음과 같이 작성해준다.
List findByName(String name, Sort sort);
이름으로 조회하며, 결과를 Sort 하도록 Sort 파라미터를 전달한다.
Sort 표현식 생성하기.
Sort 객체를 전달하기 위해서는 아래와 같이 2가지 방법을 이용할 수 있다.
TypeSafe 방식으로 생성하기.
private Sort getUserSortByCreatedAt(String ascdes) {
Sort.TypedSort<User> userSortType = Sort.sort(User.class);
Sort.TypedSort<LocalDateTime> sortBy = userSortType.by(User::getCreatedAt);
if ("DES".equals(ascdes)) {
return sortBy.descending();
}
return sortBy.ascending();
}
이 메소드는 Sort.TypedSort 라는 typeSafe 형태로 생성한 것이다 .
위와 같이 Sort 를 생성하면, code 는 약간 장황하지만, 버그 없는 코드를 만들 수 있다.
필드 이름을 직접 스트링으로 해서 생성.
private Sort getUserSortByCreatedAtV2(String ascdes) {
if ("DES".equals(ascdes)) {
return Sort.by("createdAt").descending();
}
return Sort.by("createdAt").ascending();
}
타임 안정성 잇는 코드보다는 간편하지만, " 안에 있는 필드 이름의 오타로 인해서 오류 유발 가능성이 더 높다.
무엇을 사용하든지 소트 객체를 만들어서 Repository 에 생성한 메소드 이름에 파라미터로 전달하면 다음과 같은 쿼리 결과를 확인할 수 있다.
Hibernate:
select
user0_.id as id1_5_,
user0_.birth as birth2_5_,
user0_.created_at as created_3_5_,
user0_.name as name4_5_
from
user user0_
where
user0_.name=?
order by
user0_.created_at desc
Paging
페이징은 비즈니스 요건으로 항상 나타나는 사항중에 하나이다.
페이징 역시 Sort와 마찬가지로 메소드 파라미터로 전달되며, 결과를 부분적으로 조회할 수 있다.
Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Pageable pageable);
위 메소드는 페이지에 대한 메소드 작성예를 보여준다.
- Page로 반환하는 경우 Page 요청 내용을 함께 저장할 수 있으므로, 현재 페이지 번호와 한번 조회할때 row 개수등 기본적인 페이징 요건도 함께 저장된다.
- Slice 는 다음 Slice 가 존재하는지에 대해서 결과를 반환하며 더보기 기능과 같은 쿼리를 작성할때 효과적일 것이다.
- List 는 단지 페이징 결과를 리스트로 반환하며, 직관적이겠지만 페이징 정보를 담지 않기 때문에 별도로 페이징 요건을 저장해야한다.
/**
* Returns a {@link Page} of entities meeting the paging restriction provided in the {@code Pageable} object.
*
* @param pageable
* @return a page of entities
*/
Page<T> findAll(Pageable pageable);
모든 사용자를 조회하고, Page 정책에 따라서 조회하도록 하고 있다. 이 쿼리는 이미 CRUDRepository 에 내장되어 있는 것을 사용할 것이다.
Page 요건 생성하기.
페이지객체를 생성하기 위해서는 다음과 같이 3가지 정적 메소드를 이용할 수 있다.
/**
* 페이지 요청객체 생성한다.
*
* @param page 0 부터 시작하는 페이지 인덱스, 음수가 되면 안된다.
* @param size 0 보다 큰 값으로 페이지당 결과 건수를 지정한다.
* @since 2.0
*/
public static PageRequest of(int page, int size) {
return of(page, size, Sort.unsorted());
}
/**
* 소트가 적용된 페이지 요청객체 생성
*
* @param page 0 부터 시작하는 페이지 인덱스, 음수가 되면 안된다.
* @param size 0 보다 큰 값으로 페이지당 결과 건수를 지정한다.
* @param sort 소트 객체를 전달한다. 널이 되면 안된다. 널이라면 Sort.unsorted() 를 전달하거나 해야한다.
* @since 2.0
*/
public static PageRequest of(int page, int size, Sort sort) {
return new PageRequest(page, size, sort);
}
/**
* Creates a new {@link PageRequest} with sort direction and properties applied.
*
* @param page 0 부터 시작하는 페이지 인덱스, 음수가 되면 안된다.
* @param size 0 보다 큰 값으로 페이지당 결과 건수를 지정한다.
* @param sort 소트 객체를 전달한다. 널이 되면 안된다. 널이라면 Sort.unsorted() 를 전달하거나 해야한다.
* @param properties 소트 대상을 넣는다. 널이 되면 안된다.
* @since 2.0
*/
public static PageRequest of(int page, int size, Direction direction, String... properties) {
return of(page, size, Sort.by(direction, properties));
}
아래와 같이 요청 서비스를 만들어보자.
public Page<UserDto> findAllUserWithPaging(int page, int sizePerPage) {
PageRequest pageReq = PageRequest.of(page, sizePerPage);
Page<User> usersWithPage = userRepository.findAll(pageReq);
log.info("Total Users: " + usersWithPage.getTotalElements());
log.info("Total Pages: " + usersWithPage.getTotalPages());
List<UserDto> users = usersWithPage.getContent().stream().map(user -> user.getDTO()).collect(Collectors.toList());
return new PageImpl<>(users, usersWithPage.getPageable(), usersWithPage.getTotalElements());
}
- PageRequest.of(page, sizePerPage): 페이지를 위한 표현식을 만든다.
- findAll(Pageable): 쿼리를 수행한다.
- userWithPage.getTotlaElements(): 전체 글의 개수를 확인할 수 있다.
- usersWithPage.getTotalPages(): 총 페이지 개수를 확인할 수 있다.
- new PageImpl<>(users, usersWithPage.getPageable(), usersWithPage.getTotalElements()) : 요청한 클라이언트로 Page 객체를 전달해야하므로, Page 객체를 생성했다.
결과 보기.
이제 쿼리와 조회 결과를 한번 살펴 볼 것이다.
Hibernate:
select
user0_.id as id1_5_,
user0_.birth as birth2_5_,
user0_.created_at as created_3_5_,
user0_.name as name4_5_
from
user user0_ limit ?,
?
mysql 로 처리가 가능한 limit ?, ? 로 쿼리가 생성되었다. 이것으로 쿼리 결과가 조회 될 것이다.
Hibernate:
select
count(user0_.id) as col_0_0_
from
user user0_
totalElement 를 확인할 수 있도록 전체 개수를 확인할 수 있다.
이 결과로 우리는 토탈 엘리먼트를 확인할 수 있고, 토탈 페이지 개수도 확인할 수 있다.
{
"content": [
{
"id": "kido3",
"name": "kido",
"birth": "770605",
"createdAt": "2020-11-16T10:15:06.339",
"userDetail": null,
"roles": null
},
{
"id": "kido4",
"name": "kido",
"birth": "770605",
"createdAt": "2020-11-16T10:15:09.611",
"userDetail": null,
"roles": null
}
],
"pageable": {
"sort": {
"unsorted": true,
"sorted": false,
"empty": true
},
"pageNumber": 1,
"pageSize": 2,
"offset": 2,
"unpaged": false,
"paged": true
},
"last": false,
"totalElements": 5,
"totalPages": 3,
"sort": {
"unsorted": true,
"sorted": false,
"empty": true
},
"first": false,
"size": 2,
"number": 1,
"numberOfElements": 2,
"empty": false
}
보는 바와 같이 위 구조는 다음과 같이 조회된다.
- content: 페이징 결과 목록을 배열로 저장되어 있다.
- pageable: 페이지 정보를 저장한다.
- sort: 소트 내역을 확인할 수 있다.
- pageNumber: 현재 페이지 번호 (우리는 1, 2를 전달했고, 현재는 2페이지의 결과 2개를 가져온 것이다.) 참고: 페이지는 0부터 시작한다.
- pageSize: 현재 페이지당 결과 개수이다.
- last: 마지막 페이지인지 여부
- totalElements: 총 엘리먼트의 개수
- totalPage: 총 페이지 개수
- first: 첫페이지 여부
- numberOfElements: 현재 쿼리 결과로 조회된 엘리먼트의 개수
위 내용만 보아도 웹에서 수행할 수 있는 대부분의 페이징 화면을 만드는데 문제가 없을 것이다.
Page, Sort 같이 이용하기.
페이징이 필요한 데이터는 대부분 Page와 Sort 를 함께 이용하게 되어 있다.
이제는 좀더 실용적으로 2개 모두 활용해 보자.
Pageable 는 소트 기준을 함께 담을 수 있는 메소드를 제공한다는 것일 이미 알 것이다. 우리는 그것을 이용할 것이다.
새롭게 메소드를 하나 추가해보자.
public Page<UserDto> findAllUserWithPagingAndSort(int page, int sizePerPage, String ascdes) {
PageRequest pageReq = PageRequest.of(page, sizePerPage, getUserSortByCreatedAtV2(ascdes));
Page<User> usersWithPage = userRepository.findAll(pageReq);
log.info("Total Users: " + usersWithPage.getTotalElements());
log.info("Total Pages: " + usersWithPage.getTotalPages());
List<UserDto> users = usersWithPage.getContent().stream().map(user -> user.getDTO()).collect(Collectors.toList());
return new PageImpl<>(users, usersWithPage.getPageable(), usersWithPage.getTotalElements());
}
달라진것은 String ascdes 라는 파라미터가 추가 되었고. PageRequest.of 에서 마지막에 Sort 객체를 전달할 수 있도록 메소드를 호출했다.
private Sort getUserSortByCreatedAtV2(String ascdes) {
if ("DES".equals(ascdes)) {
return Sort.by("createdAt").descending();
}
return Sort.by("createdAt").ascending();
}
메소드는 이전에도 살펴본 소트 메소드 이다.
결과 확인하기.
Hibernate:
select
user0_.id as id1_5_,
user0_.birth as birth2_5_,
user0_.created_at as created_3_5_,
user0_.name as name4_5_
from
user user0_
order by
user0_.created_at desc limit ?,
?
order by 와 limit 가 정상으로 걸린것을 확인할 수 있다.
이것 이외에 이전과 동일하다.
{
"content": [
{
"id": "kido1",
"name": "kido",
"birth": "770605",
"createdAt": "2020-11-16T10:35:37.107",
"userDetail": {
"id": "kido1",
"nick": "kaido",
"avatarImg": "http://kido.com/img.png",
"category": "economic",
"joinedAt": "2020-11-16T10:35:37.107",
"modifiedAt": "2020-11-16T10:35:37.107"
},
"roles": null
}
],
"pageable": {
"sort": {
"unsorted": false,
"sorted": true,
"empty": false
},
"offset": 2,
"pageNumber": 1,
"pageSize": 2,
"paged": true,
"unpaged": false
},
"last": true,
"totalElements": 3,
"totalPages": 2,
"first": false,
"size": 2,
"number": 1,
"sort": {
"unsorted": false,
"sorted": true,
"empty": false
},
"numberOfElements": 1,
"empty": false
}
실행 결과 반환값을 확인하면 pageable.sort.sorted 값이 true 로 되어 있음을 확인할 수 있다.
sort 실행 결과가 정상으로 수행되었다는 것을 Page 내의 속성값을 통해 확인이 가능하다.
Wrap up
지금까지 derived query 를 확인해 보았다.
간단한 쿼리나, 일반적인 쿼리 대부분은 derived query 로 쉽게 생성이 가능하다.
또한 페이징과 소팅을 확인해 보았으며, 결과를 어떻게 활용하면 좋을지도 이해할 수 있다.
'JPA' 카테고리의 다른 글
[JPA] JPA 기초 12 Propagation, Isolation 알아보기 (0) | 2022.04.12 |
---|---|
[JPA] JPA 기초 10 Entity Lifecycle (0) | 2022.04.12 |
[JPA] JPA 기초 09 Entity Event Hooking (0) | 2022.04.12 |
[JPA] JPA 기초 08 ManyToMany with Entity (0) | 2022.04.12 |
[JPA] JPA 기초 07 ManyToMany (0) | 2022.04.12 |
- Total
- Today
- Yesterday
- kubectl
- springboot
- mapping
- CD
- docker
- deploy
- mongo
- jenkins
- Spring
- declative
- AWS
- NodeSelector
- CI
- go
- kafka-springboot
- gitops
- jpa
- java
- argocd
- kubernetes
- Gorilla
- Terraform
- tfsec
- MongoDB
- docker-compose
- Kafka
- MySQL
- Golang
- D3
- Database
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |