저번에 Querydsl 설정을 알아보았다. 이제 직접 사용해보자.
JpaRepository를 custom 했다는 컨벤션으로 ~~ RepositoryCustom을 만들어 해당 Repository를 상속하고, ~~ Impl로 구현체로 만드는 방식을 선택할 수도 있습니다. 하지만 Querydsl을 사용하기 위한 JPAQueryFactory를 Bean으로 만들고 @Repository 어노테이션을 사용하시는 게 더 좋습니다.
ex)
- MemberRepository extends JpaRepository
- MemberRepositoryCustom extends MemberRepository
- MemberRepositoryImpl implmentation MemberRepositoryCustom
위처럼 사용하지 않습니다.
아래처럼 @Bean
으로 등록해서 사용하면 됩니다.
@Configuration
@RequiredArgsConstructor
public class QuerydslConfig {
private final EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em){
return new JPAQueryFactory(em);
}
}
@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl{
private final JPAQueryFactory factory;
// 중략
}
Test 코드를 작성하는 곳에 시작합니다. **세팅**을 먼저 해줍니다. 주석으로 설명을 달겠습니다.
아래의 엘리어스(별칭)를 주면서 Q객체를 사용할 수 있습니다.
QMember member = new QMember("member");
🎈 하지만 Q객체들은 static으로 접근할 수 있으므로 아래와 같이 같은 Q객체를 여러번 사용하지 않는 이상 static을 import 해서 사용합니다.
QMember.member;
// QMember를 static import
⬇
member;
아래와 같이 jpaQueryFactory
의 메서드들을 이용합니다. 메서드들의 이름으로 직관적으로 어떠한 역할을 하는지 알 수 있습니다. select
와 from
을 나눠서 작성할 수도 있고, 합쳐서 selectFrom
으로 작성할 수도 있습니다.
@Test
void select(){
Member result1 = jpaQueryFactory.select(member)
.from(member).fetchOne();
Member result2 = jpaQueryFactory.selectFrom(member)
.fetchOne();
}
위에서 fetchOne()
을 붙여서 단 건 조회를 했습니다. 아래와 같이 반환인자를 결정할 수 있습니다.
🎈 fetchOne()
: 단건 조회 여러 개가 조회되면 오류 발생
🎈 fetchFirst()
: 단 건 조회이 여러 개가 조회돼도 첫 번째 값 반환
🎈 fetch()
: List 형태로 반환
😓 fetchResult
, fetchCount
: 두 개가 deprecated
되었습니다. 각각의 역할을 할 수 있는 방법은 밑에서 페이징 관련하면서 알아보겠습니다.
@Test
void returnResult(){
Member result1 = jpaQueryFactory.selectFrom(member)
.where(member.name.eq("hi1"))
.fetchOne();
Member result2 = jpaQueryFactory.selectFrom(member)
.fetchFirst();
List<Member> result3 = jpaQueryFactory.selectFrom(member)
.fetch();
}
where(컬럼.eq(조건))
이고, eq는 ==
이라고 생각하시면 됩니다. @Test
void findName(){
Member result = jpaQueryFactory.select(member)
.from(member)
.where(member.name.eq("hi1"))
.fetchOne();
assertThat(result.getAge()).isEqualTo(10);
}
@Test
void findNameAndAge(){
Member result1 = jpaQueryFactory.select(member)
.from(member)
.where(member.name.eq("hi1").and(member.age.eq(10)))
.fetchOne();
Member result2 = jpaQueryFactory.select(member)
.from(member)
.where(member.name.eq("hi1"),member.age.eq(10))
.fetchOne();
assertThat(result1.getAge()).isEqualTo(10);
assertThat(result2.getAge()).isEqualTo(10);
}
🎈 where 조건 안에서 ,
로 구분하는 것이 깔끔 !!
@Test
void notEqual(){
List<Member> results = jpaQueryFactory.selectFrom(member)
.where(member.name.ne("hi1")).fetch();
assertThat(results.size()).isEqualTo(3);
}
in()
메서드 안에 값들을 넣어주면 됩니다. 예제는 나이가 10,11인 Member를 찾는 것입니다. 2명이 조회됩니다.@Test
void in(){
List<Member> results = jpaQueryFactory.selectFrom(member)
.where(member.age.in(10, 11)).fetch();
assertThat(results.size()).isEqualTo(2);
}
like연산은 "%3"이라면 3으로 끝나는 것을 조회하는 것입니다. 반대로 "3%"라면 3으로 시작하는 것
예제에서는 "%3"으로 해서 이름이 3으로 끝나는 멤버를 찾는 것입니다.
@Test
void like(){
Member result = jpaQueryFactory.selectFrom(member)
.where(member.name.like("%3")).fetchOne();
assertThat(result.getName()).isEqualTo("hi3");
}
@Test
void contains(){
Member result = jpaQueryFactory.selectFrom(member)
.where(member.name.contains("3"))
.fetchOne();
assertThat(result.getName()).isEqualTo("hi3");
}
orderBy를 이용해서 정렬 조건을 줄 수 있습니다. 예제에서는 나이를 내림차순으로 null값을 맨 뒤로 보내는 정렬입니다.
orderBy를 여러 개 사용함으로써 정렬 조건을 추가할 수 있습니다.
@Test
void sort(){
List<Member> fetch = jpaQueryFactory.selectFrom(member)
.orderBy(member.age.desc().nullsLast())
//. orderBy(member.name.asc())
.fetch();
assertThat(fetch.get(0).getAge()).isEqualTo(13);
}
limit
과 offset
을 이용해서 페이징을 할 수 있습니다. 여기서 limit은 페이지의 크기이고, offset은 시작점입니다.
예제에서는 나이를 내림 차순으로 정렬하고, 0번째 인덱스부터 시작하는데 2개씩 자르는 것입니다.
즉 13 12 | 11 10
로 페이지가 나눠진 형태이고, 13 12가 선택된 것입니다.
@Test
void page(){
List<Member> result = jpaQueryFactory.selectFrom(member)
.orderBy(member.age.desc())
.offset(0)
.limit(2)
.fetch();
assertThat(result.size()).isEqualTo(2);
assertThat(result.get(0).getName()).isEqualTo("hi4");
}
querydsl
에서는 데이터베이스에서 사용하는 대부분의 내장 함수
를 사용할 수 있습니다. 예제는 sum
, avg
, count
, min
, max
를 구하는 것입니다. tuple
에 들어있는 값을 얻기 위해선 select에서 사용한 명칭을 그대로 사용하면 됩니다.@Test
void aggregation(){
List<Tuple> tuples = jpaQueryFactory.select(member.age.sum(),
member.age.avg(),
member.age.count(),
member.age.max(),
member.age.min()).from(member)
.fetch();
assertThat(tuples.size()).isEqualTo(1);
Tuple result = tuples.get(0);
assertThat(result.get(member.age.count())).isEqualTo(4);
assertThat(result.get(member.age.max())).isEqualTo(13);
assertThat(result.get(member.age.min())).isEqualTo(10);
}
groupBy
메서드와 having
메서드를 그대로 사용합니다. 예제에서는 Member의 나이별로 묶었고 having에서 주어진 조건은 age별로 묶었을 때 개수가 2보다 크거나 같은 것들을 조회하는 것입니다. 따라서 나이가 10인 그룹만 조회됐습니다.@Test
void groupBy(){
memberRepository.save(createMember("hi5",10));
memberRepository.save(createMember("hi6",10));
List<Long> result = jpaQueryFactory.select(member.age.count())
.from(member)
.orderBy(member.age.asc())
.groupBy(member.age)
.having(member.age.count().goe(2))
.fetch();
assertThat(result.get(0)).isEqualTo(3);
assertThat(result.size()).isEqualTo(1);
}
join
메서드를 사용합니다. join(조인할 대상, 조인 별칭)
입니다. 예제에서는 Member의 club을 조인하고, club의 이름이 smu인 것을 조회합니다.@Test
void join(){
List<Member> result =jpaQueryFactory.selectFrom(member)
.join(member.club, club)
.where(club.name.eq("smu"))
.fetch();
}
🎈 패치 조인을 사용하려면 뒤에 패치 조인만 붙여주면 됩니다.
@Test
void fetchJoin() {
List<Member> result = jpaQueryFactory.selectFrom(member)
.join(member, club).fetchJoin()
.where(club.name.eq("smu"))
.fetch();
}
🎈 세타 조인도 지원합니다.
@Test
void thetaJoin(){
List<Member> result =jpaQueryFactory.selectFrom(member)
.join(member,club)
.where(member.name.eq(club.name))
.fetch();
}
🎈 on 조인도 지원합니다.
@Test
void onJoin(){
List<Tuple> result =jpaQueryFactory.selectFrom(member)
.join(member,club)
.leftJoin(group).on(member.name.eq(group.name))
.fetch();
}
서브 쿼리는 from
절을 제외하고, select, where절에서 사용할 수 있습니다. JPAExpressions를 이용합니다. 이때 같은 테이블을 서브 쿼리에서 사용한다면 위에서 알아봤었던 별칭을 이용해서 별도로 Q객체를 하나 더 만들어야 합니다.
예제에서는 서브 쿼리를 이용해서 가장 나이가 많은 멤버를 찾는 것입니다. JPAExpressions도 static import해서 사용하면 됩니다.
@Test
void subQuery() {
QMember subMember = new QMember("subMember");
List<Member> result = jpaQueryFactory.selectFrom(member)
.where(member.age.eq(JPAExpressions.select(subMember.age.max()).from(subMember)))
.fetch();
assertThat(result.get(0).getAge()).isEqualTo(13);
}
case문을 when, then으로 사용할 수 있습니다. 컬럼.when(조건).then(행동).whe(조건).then(행동).otherwise(행동)
예제에서는 Member의 나이를 가지고 age=10 은 10살, age=11은 11살 그 외에 기타로 했습니다.
@Test
void caseWhen() {
List<String> result = jpaQueryFactory.
select(member.age
.when(10).then("10살")
.when(11).then("11살")
.otherwise("기타"))
.from(member)
.orderBy(member.name.asc()).fetch();
assertThat(result.get(3)).isEqualTo("기타");
}
간단한 조건이 아닌 복잡한 조건을 가질 때는 CaseBuilder
를 사용합니다. 위의 예제를 조금 변형해서 10 <=age <=11일 때는 10~11살 나머지는 기타로 하기 원한다면 아래와 같이 작성해주시면 됩니다.
@Test
void caseWhen2() {
List<String> result = jpaQueryFactory
.select(new CaseBuilder()
.when(member.age.between(10, 11)).then("10~11살")
.otherwise("기타"))
.from(member)
.orderBy(member.name.asc()).fetch();
assertThat(result.get(3)).isEqualTo("기타");
}
상수를 선언하고 싶다면 Expressions.constant
메서드를 사용합니다. 예제에서는 상수 "A" 만들도록 하겠습니다. 마찬가지로 static import 하셔서 사용하면 편리합니다.
@Test
void constEx() {
Tuple tuple = jpaQueryFactory
.select(member.name, Expressions.constant("A"))
.from(member)
.fetchFirst();
assertThat(tuple.get(member.name)).isEqualTo("hi1");
assertThat(tuple.get(Expressions.constant("A"))).isEqualTo("A");
}
문자열을 별도로 붙이고 싶다면 concat을 이용합니다. 이때 String이 아니라면 stringValue()
를 사용하셔야 합니다. enum, 숫자, 날짜 등등에 사용할 수 있습니다. 예제에서는 이름+_+나이로 바꿨습니다.
@Test
void concat() {
String result = jpaQueryFactory
.select(member.name.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.name.eq("hi1"))
.fetchOne();
assertThat(result).isEqualTo("hi1_10");
}
JPA에서는 프로젝션을 하기 위해선 디렉터리 명을 모두 적었어야 했습니다. 하지만 querydsl에서는 쉽게 사용할 수 있습니다. Entity를 반환하는 것은 필요 없는 필드 값들도 있으므로 성능 저하가 있을 수 있습니다. 따라서 프로젝션을 사용하는 것이 성능 면에서 이로울 수 있습니다.
@Data
@NoArgsConstructor
public class Dto {
public String username;
public int age;
@QueryProjection
public Dto(String username, int age){
this.username=username;
this.age=age;
}
}
🎈 이제 프로젝션 방법에 대해서 알아보겠습니다.
Projections.bean
을 이용하면 setter로 접근해서 필드 값들을 채워줍니다. Entity의 필드명과 Dto의 필드명이 같다면 그대로 사용해도 되지만, 필드명이 다르다면 as를 사용해야 합니다. 예제에서는 member에서는 name, Dto에서는 username이라 as를 사용합니다.
Dto result1 = jpaQueryFactory
.select(Projections.bean(Dto.class,
member.name.as("username"),
member.age))
.from(member)
.where(member.name.eq("hi1"))
.fetchOne();
Projections.field
를 사용하면 필드에 직접 접근할 수 있습니다.
Dto result2 = jpaQueryFactory
.select(Projections.fields(Dto.class,
member.name.as("username"),
member.age))
.from(member)
.where(member.name.eq("hi1"))
.fetchOne();
Projections.constructor
를 사용하면 생성자를 이용해서 접근합니다.
Dto result3 = jpaQueryFactory
.select(Projections.constructor(Dto.class,
member.name.as("username"),
member.age))
.from(member)
.where(member.name.eq("hi1"))
.fetchOne();
프로젝션 할 객체의 생성자 위에
@QueryProjection
을 붙이면 해당 객체 또한 Q객체를 생성합니다. 따라서 new를 이용해서 프로젝션 진행이 가능합니다. 간단하지만, Dto에 코드 침투가 일어나므로 해당 부분은 고려해야 할 사항입니다.
Dto result4 = jpaQueryFactory
.select(new QDto(member.name, member.age))
.from(member)
.where(member.name.eq("hi1"))
.fetchOne();
distinct
메서드를 이용해 적용할 수 있습니다.@Test
void distinct() {
List<String> fetch = jpaQueryFactory.select(member.name).distinct()
.from(member)
.fetch();
}
🎈 JPQL에서는 동적 쿼리를 문자열 방식으로 처리해 많은 불편함이 있었습니다. querydsl
에서는 BooleanBuilder
와 Where
절 다중 파라미터를 이용해서 처리할 수 있습니다. 더 많이 사용하는 방식인 where절 다중 파라미터에 대해서만 알아보겠습니다.
🎈 where
절에 BooleanExpreesion
을 반환하는 메서드를 사용합니다. 그리고 nameEq, ageEq에서는 값이 없다면 null을 반환하고, 아닐 경우에는 값을 반환합니다. where
절에서는 null 값인 경우에 조건을 무시합니다. 따라서 동적 쿼리를 완성할 수 있습니다.
@Test
void dynamicQuery() {
List<Member> hi1 = findUser("hi1", 10);
assertThat(hi1.size()).isEqualTo(1);
}
private List<Member> findUser(String nameCondition, Integer ageCondition) {
return jpaQueryFactory
.selectFrom(member)
.where(nameEq(nameCondition), ageEq(ageCondition))
.fetch();
}
private BooleanExpression nameEq(String nameCondition) {
return nameCondition != null ? member.name.eq(nameCondition) : null;
}
private BooleanExpression ageEq(Integer ageCondition) {
return ageCondition != null ? member.age.eq(ageCondition) : null;
}
update
의 경우 update set
을 사용하고 마지막에 execute를 붙여 바뀐 개수를 반환받습니다. 이때 주의할 점은 bulk 연산은 영속성 콘텍스트가 아닌 바로 데이터베이스에 접근하므로 영속성 콘텍스트에 남아있는 변경 사항과 값들을 데이터 베이스에 반영하고 초기화해줘야 합니다.
@Test
void update() {
long count = jpaQueryFactory
.update(member)
.set(member.age, member.age.add(10))
.where(member.name.eq("hi1"))
.execute();
em.flush();
em.clear();
}
조회된 데이터의 내용과 전체 개수를 구하는 쿼리는 따로 작성하는 것이 성능상 좋습니다.
여기서 이전에는 개수까지 한 번에 세는 fetchResults
를 사용하는 것은 성능에 영향을 끼칠 수도 있어서 사용하지 않기도 하고, 모든 쿼리에 대해서 개수를 정확하게 내지 않아서 deprecated
되었습니다.
fetchCount
는 개수를 세는 메서드입니다. deprecated
되었습니다. 🎈 따라서 fetchResult가 아닌 fetch를 사용하고, fetchCount가 아닌 fetch(). size()를 사용하라고 주석으로 달려있습니다.
fetch(). size()
가 아닌 Wildcart.count
를 활용하는 것이 더 깔끔합니다.
이렇게 해서 데이터와 전체 개수를 구했다면, PageableExecutonUtils
를 이용합니다. 이것은 Data-JPA에서 제공해주는 함수입니다. 아래와 같은 역할을 해줍니다.
count 쿼리가 생략 가능한 경우 생략해서 처리
따라서 아래와 같이 최적화를 진행할 수 있습니다.
@Test
void utilPage() {
PageRequest pageable = PageRequest.of(0, 2);
List<Member> content = jpaQueryFactory
.selectFrom(member)
.limit(pageable.getPageSize())
.offset(1)
.fetch();
Long totalCount = jpaQueryFactory.select(Wildcard.count)
.from(member)
.fetch().get(0);
Page<Member> page = PageableExecutionUtils.getPage(content, pageable, () -> totalCount);
List<Member> results = page.getContent();
int totalPages = page.getTotalPages();
boolean hasNext = totalPages > pageable.getPageNumber();
assertThat(hasNext).isTrue();
assertThat(totalPages).isEqualTo(2);
assertThat(results.size()).isEqualTo(2);
}