Querydsl vs JPQL
//JPQL
String qlString = "select m from Member m " + "where m.username = :username";
//Querydsl
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1"))//파라미터 바인딩 처리
.fetchOne();
실행되는 JPQL 보기
spring.jpa.properties.hibernate.use_sql_comments: true
검색 쿼리
@Test
public void search() {
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1")
.and(member.age.eq(10)))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
where() 절에 검색조건 추가 가능
List<Member> result1 = queryFactory
.selectFrom(member)
.where(member.username.eq("member1"), member.age.eq(10))
.fetch();
※ 결과 조회
- fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
- fetchOne() : 단 건 조회
결과가 없으면 : null
결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException - fetchFirst() : limit(1).fetchOne()
- fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
- fetchCount() : count 쿼리로 변경해서 count 수 조회
정렬
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
- nullsLast() , nullsFirst() : null 데이터 순서 부여
페이징
조회 건수 제한
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) //0부터 시작(zero index)
.limit(2) //최대 2건 조회
.fetch(); //count 쿼리 X
.fetchResults(); //count 쿼리 함께 실행
☆ .fetchResults() 주의: count 쿼리가 실행되니 성능상 주의! 데이터 조회는 여러 테이블 조인이 필요하지만 count 쿼리는 조인이 필요 없는 경우도 있다. count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, count 전용 쿼리를 별도로 작성해야 한다.
집합
집합 함수
List<Tuple> result = queryFactory
.select(member.count(),
member.age.sum(),
member.age.avg(),
member.age.max(),
member.age.min())
.from(member)
.fetch();
GroupBy, Having 사용
List<Tuple> result = queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.having(item.price.gt(1000))
.fetch();
조인
기본 조인
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
세타 조인 - 카티디안 곱
List<Member> result = queryFactory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
- 외부 조인 불가능 → on 절 사용하면 가능
On 절
1. 조인 대상 필터링
//회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(member.team, team).on(team.name.eq("teamA"))
.fetch();
- 내부 조인(Inner Join) : where()로 해결
- 외부 조인(Outer Join) : On 절로 해결
2. 연관관계 없는 엔티티 외부 조인
//회원의 이름과 팀의 이름이 같은 대상 외부 조인
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(team).on(member.username.eq(team.name))
.fetch();
- 일반조인: leftJoin(member.team, team)
- on조인: from(member).leftJoin(team).on(xxx)
페치 조인(.fetchJoin())
페치 조인 적용
Member findMember = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
서브 쿼리 (JPAExpressions)
서브 쿼리 eq, goe ... 사용 가능
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub)))
.fetch();
서브쿼리 여러 건 처리 in 사용
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.in(
JPAExpressions
.select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))))
.fetch();
select 절에 subquery (스칼라 서브 쿼리)
List<Tuple> fetch = queryFactory
.select(member.username,
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub))
.from(member)
.fetch();
※ from 절의 서브쿼리 한계 (인라인 뷰)
JPQL의 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 따라서 Querydsl도 지원되지 않는다.
☆ from 절의 서브쿼리 해결방안
- 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
- 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
- nativeSQL을 사용한다.
Case 문 (CaseBuilder())
복잡한 조건
List<String> result = queryFactory
.select(new CaseBuilder()
.when(member.age.between(0, 20)).then("0~20살")
.when(member.age.between(21, 30)).then("21~30살")
.otherwise("기타"))
.from(member)
.fetch();
Case 문 + OrderBy
// 1. 0 ~ 30살이 아닌 회원 출력
// 2. 0 ~ 20살 회원 출력
// 3. 21 ~ 30살 회원 출력
NumberExpression<Integer> rankPath = new CaseBuilder()
.when(member.age.between(0, 20)).then(2)
.when(member.age.between(21, 30)).then(1)
.otherwise(3);
List<Tuple> result = queryFactory
.select(member.username, member.age, rankPath)
.from(member)
.orderBy(rankPath.desc())
.fetch();
상수, 문자 더하기
상수 필요시 `Expressions.constant(xxx)` 사용
Tuple result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetchFirst();
// 결과
tuple = [member1, A]
tuple = [member2, A]
tuple = [member3, A]
tuple = [member4, A]
문자 더하기 concat
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
// 결과
member1_10
- .stringValue() : 다른 타입을 문자로 변환. 특히 ENUM을 처리 자주 사용
중급 문법
프로젝션
// 대상이 하나
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
// 대상이 둘 이상 (튜플)
List<Tuple> result = queryFactory
.select(member.username, member.age)
.from(member)
.fetch();
DTO 반환
1. 프로퍼티 접근 - Setter
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class, member.username, member.age))
.from(member)
.fetch();
2. 필드 직접 접근
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class, member.username,member.age))
.from(member)
.fetch();
2-1. 별칭이 다를 때
List<UserDto> fetch = queryFactory
.select(Projections.fields(UserDto.class, member.username.as("name"),
ExpressionUtils.as(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")
)
).from(member)
.fetch();
- ExpressionUtils.as(source,alias) : 필드나, 서브 쿼리에 별칭 적용
- username.as("memberName") : 필드에 별칭 적용
3. 생성자 사용
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class, member.username, member.age))
.from(member)
.fetch();
}
프로젝션과 결과 반환 (@QueryProjection)
생성자 + @QueryProjection
//MemberDto의 생성자에 @QueryProjection 추가
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
- './gradlew compileQuerydsl' : QMemberDto 생성
@QueryProjection 활용 & distinct
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age)).distinct()
.from(member)
.fetch();
@QueryProjection 장단점
- 장점 : 컴파일러로 에러 체크 가능
- 단점 : Dto가 Querydsl 기술에 종속됨
동적 쿼리 (BooleanBuilder)
1. BooleanBuilder
private List<Member> searchMember1(String usernameCond, Integer ageCond) {
BooleanBuilder builder = new BooleanBuilder();
if (usernameCond != null) {
builder.and(member.username.eq(usernameCond));
}
if (ageCond != null) {
builder.and(member.age.eq(ageCond));
}
return queryFactory
.selectFrom(member)
.where(builder)
.fetch();
}
2. Where 다중 파라미터 사용
//사용
List<Member> result = searchMember2(usernameParam, ageParam);
//동적 쿼리 메서드
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameCond), ageEq(ageCond))
.fetch();
}
private BooleanExpression usernameEq(String usernameCond) {
return usernameCond != null ? member.username.eq(usernameCond) : null;
}
private BooleanExpression ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null;
}
//조합 가능
private BooleanExpression allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
수정, 삭제 벌크 연산 (em.flush(), em.clear())
기존 숫자에 +1, -1
long count = queryFactory
.update(member)
.set(member.age, member.age.add(1))
// .set(member.age, member.age.add(-1))
.execute();
- '.minus()' 기능 없음
- 'multiply(x)' : 곱하기
쿼리 한번으로 대량 데이터 삭제
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
SQL function 호출하기
member M으로 변경하는 replace 함수 사용
String result = queryFactory
.select(Expressions.stringTemplate("function('replace', {0}, {1}, {2})", member.username, "member", "M"))
.from(member)
.fetchFirst();
- ansi 표준 함수들은 querydsl이 대부분 내장하고 있다.
.where(member.username.eq(member.username.lower()))
.where(member.username.eq(Expressions.stringTemplate("function('lower', {0})", member.username)))
- 1번 : querydsl, 2번 : SQL function
동적 쿼리와 성능 최적화 조회 (Where절 파라미터 사용)
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id, member.username, member.age,
team.id, team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetch();
}
private BooleanExpression usernameEq(String username) {
return isEmpty(username) ? null : member.username.eq(username);
}
private BooleanExpression teamNameEq(String teamName) {
return isEmpty(teamName) ? null : team.name.eq(teamName);
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe == null ? null : member.age.goe(ageGoe);
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe == null ? null : member.age.loe(ageLoe);
}
조회 API 컨트롤러 개발
사용자 정의 리포지토리 구성
1. 사용자 정의 인터페이스 작성
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
2. 사용자 정의 인터페이스 구현
public class MemberRepositoryImpl implements MemberRepositoryCustom {
...
@Override
//회원명, 팀명, 나이(ageGoe, ageLoe)
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id, member.username, member.age,
team.id, team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetch();
}
private BooleanExpression usernameEq(String username) {
return isEmpty(username) ? null : member.username.eq(username);
}
private BooleanExpression teamNameEq(String teamName) {
return isEmpty(teamName) ? null : team.name.eq(teamName);
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe == null ? null : member.age.goe(ageGoe);
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe == null ? null : member.age.loe(ageLoe);
}
}
3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
★ 정리 : MemberRepositoryCustom 안에 모든 기능에 다 몰아넣지 말고 핵심 biz 기능과 아닌 것 분리
- 간단한 CRUD : MemberRepository (스프링 데이터 JPA)
- 다양한 곳에서 사용 : MemberRepositoryCustom + MemberRepositoryImpl
- 한 화면에 특화된 기능 : MemberQueryRepository
스프링 데이터 페이징 활용
result + count 쿼리 한번에 조회 (fetchResults() 사용)
@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
QueryResults<MemberTeamDto> results = queryFactory
.select(new QMemberTeamDto(
member.id, member.username, member.age,
team.id, team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total);
}
- 실제 쿼리는 count & result 따로 2번 나감!
result & count 쿼리 따로 조회
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> content = queryFactory
.select(new QMemberTeamDto(
member.id, member.username, member.age,
team.id, team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
long total = queryFactory
.select(member)
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetchCount();
return new PageImpl<>(content, pageable, total);
}
- count 쿼리와 result 쿼리 분리!
- count 쿼리에서 join이 필요없다면 성능 향상 가능
CountQuery 최적화
PageableExecutionUtils.getPage()
JPAQuery<Member> countQuery = queryFactory
.select(member)
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()));
// return new PageImpl<>(content, pageable, total);
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);
- count 쿼리가 생략 가능한 경우 생략해서 처리
- 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
ex) 전체데이터 = 100, size = 200 → 2번째 페이지가 없음! ▶ Count 쿼리 생략!! - 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)
이후 내용 : 제약이 커서 사용하기 어렵!
QuerydslPredicateExecutor
리포지토리에 적용
interface MemberRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
※ 한계점
- 조인X (묵시적 조인은 가능하지만 left join이 불가능하다.)
- 클라이언트가 Querydsl에 의존해야 한다. 서비스 클래스가 Querydsl이라는 구현 기술에 의존해야 한다.
Querydsl Web 지원
공식 URL: https://docs.spring.io/spring-data/jpa/docs/2.2.3.RELEASE/reference/html/#core.web.type-safe
※ 한계점
- 단순한 조건만 가능
- 조건을 커스텀하는 기능이 복잡하고 명시적이지 않음
- 컨트롤러가 Querydsl에 의존
QuerydslRepositorySupport
※ 장점
- EntityManager 제공
※ 한계점
- Querydsl 4.x에 나온 JPAQueryFactory로 시작할 수 없음
- select로 시작할 수 없음 (from으로 시작해야함)
※ 한계 극복을 위해 직접 Querydsl 지원 클래스 직접 만들 수 있다.
→ Querydsl4RepositorySupport 상속받고 구현하면 됨.