6 분 소요

환경설정

스프링 부트 3.0 이상 버전에서 적용해봤는데 다음 내용을 고려해주니 문제없이 동작했다!

  • Java 17 사용
    • IDE에서도 Java 버전과 관련된 설정이 잘 돼있는지 꼭 확인하기
  • H2 데이터베이스 2.1.214 버전 사용

build.gradle에도 querydsl 코드를 추가해줘야 한다.

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.1.0'
	id 'io.spring.dependency-management' version '1.1.0'
	//querydsl 추가
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

group = 'study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	// Querydsl 추가
	implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

tasks.named('test') {
	useJUnitPlatform()
}

//querydsl 추가
def querydslDir = "src/main/generated"
querydsl { jpa = true
	querydslSourcesDir = querydslDir
}
sourceSets {
	main.java.srcDir querydslDir
}
configurations {
	querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
	options.annotationProcessorPath = configurations.querydsl
}
  • querydslDir에 있는 내용은 git으로 관리되지 않도록 설정해줬음 (.gitignore에서 설정)

querydsl에서 내가 만든 엔티티를 활용하려면 우측에 있는 [Gradle] - [compileQuerydsl]을 눌러서 컴파일 해주자.
그러면 위에서 설정한 querydslDir에 파일이 생성될 것이며 (Member 엔티티라면 QMember가 생성됨) querydsl
설정이 끝난다!

문법

jpql과 거의 유사하나 직접 쿼리를 작성하지 않고 함수 호출을 통해 작성하게 된다. 대략적인 폼은 같다!

@PersistentContext
EntityManager em;

JPAQueryFactory queryFactory = new JPAQueryFactory(em);

QMember m = new QMember("m");
Member findMember = queryFactory
              .select(m)
              .from(m)
              .where(m.username.eq("member1")) //파라미터 바인딩
              .fetchOne();

assertThat(findMember.getUsername()).isEqualTo("member1");
  • emJPAQueryFactory 생성자 파라미터로 넣어 객체 생성
    • querydsl에선 모든 쿼리를 JPAQueryFactory에서 시작함
    • 한 번만 생성
  • QMember를 생성해 쿼리 파라미터로 활용
    • m 객체를 new QMember("m") 대신 QMember.member로 대체 가능
    • QMember.member(=기본 인스턴스)는 static import를 적용해 member로 줄여서 사용 가능
    • 여러 Member 객체가 필요하다면 new QMember()로 생성해서 사용해야 함
  • 쿼리에서 select/from/where절을 함수 형태로 호출 == 자바 코드 사용!
  • querydsl == jpql 빌더

검색 조건

AND, OR 등의 조건은 and(), or()를 메서드 체인으로 연결할 수 있다.
= 연산은 eq()로 처리한다.

queryFactory
  .selectFrom(member)
  .where(member.username.eq("member1")
  .and(member.age.eq(10)))
  .fetchOne();
  • select()from()selectFrom()으로 합칠 수 있고 같은 내용이 들어갈 때만 사용 가능
  • where()에서 null은 무시

결과 조회

List<Member> fetch = queryFactory
        .selectFrom(member)
        .fetch();

Member fetchOne = queryFactory
        .selectFrom(member)
        .fetchOne();

Member fetchFirst = queryFactory
        .selectFrom(member)
        .fetchFirst();

QueryResults<Member> results = queryFactory
        .selectFrom(member)
        .fetchResults();

long count = queryFactory
        .selectFrom(member)
        .fetchCount();
  • fetch() : 리스트 조회
  • fetchOne() : 단 건 조회
    • 없으면 null 리턴
    • 여러개 있으면 NonUniqueResultException 발생
  • fetchFirst() : 조건 만족하는 것 중 하나만 조회
  • fetchResults() : 페이징 정보 포함(= count 쿼리 추가 실행)
  • fetchCount() : count 수 조회

정렬

where() 뒤에 orderBy()를 호출해서 설정할 수 있다.

List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.eq(100))
                .orderBy(member.age.desc(), member.username.asc().nullsLast())
                .fetch();
  • desc()는 내림차순, asc()는 오름차순(기본값)
  • nullsLast() : null이면 마지막으로 보냄

페이징

queryFactory
    .selectFrom(member)
    .orderBy(member.username.desc())
    .offset(1)
    .limit(2)
    .fetch();
  • offset() : 시작할 인덱스 설정(0부터 시작)
  • limit() : 조회할 대상 개수 설정

fetch()대신 fetchResults()를 썼다면 페이징 정보를 얻을 수 있지만 count 쿼리도 같이 나가게 되므로 성능에
영향을 미칠 수 있다. 성능 최적화가 필요하다면 count 전용 쿼리를 따로 만들어 사용해야 한다.

집합

합, 평균, 최대, 최소는 sum(), avg(), max(), min()으로 처리할 수 있다.

queryFactory
    .select(
            member.count(),
            member.age.sum(),
            member.age.avg(),
            member.age.max(),
            member.age.min()
    )
    .from(member)
    .fetch();


group by도 GroupBy()로 처리가 가능하다! 뒤에 having()도 사용할 수 있다.

queryFactory
    .select(
            team.name,
            member.age.avg()
    )
    .from(member)
    .join(member.team, team)
    .groupBy(team.name)
    .fetch();
  • join()으로 memberteam을 inner join 할 수 있음
  • teamQTeam.team(=기본 인스턴스)을 의미

조인

inner join, left/right outer join 모두 사용 가능하다.
inner join은 join() 또는 innerJoin()을,
outer join은 방향에 맞게 leftJoin(), rightJoin()을 사용하면 된다.
조건 설정은 뒤에 where()이나 on()에서 해주면 된다.

queryFactory
    .selectFrom(member)
    .join(member.team, team)
    .where(team.name.eq("teamA"))
    .fetch();


세타 조인은 join()을 사용하지 않고 from에 엔티티를 바로 적은 뒤 where()에서 조건 설정해주면 된다.

queryFactory
    .select(member)
    .from(member, team)
    .where(member.username.eq(team.name))
    .fetch();


엔티티 간 연관관계가 없어도 조인을 할 수 있다. 연관된 필드가 없으므로 outer join을 해줘야 하며,
on()을 활용해 조건을 설정할 수 있다.

queryFactory
    .select(member, team)
    .from(member)
    .leftJoin(team).on(member.username.eq(team.name))
    .fetch();


jpql에서는 페치 조인을 쿼리 안에 join fetch를 써줬지만, querydsl에서는 fetchJoin()을 사용한다.

queryFactory
    .selectFrom(member)
    .join(member.team, team).fetchJoin()
    .where(member.username.eq("member1"))
    .fetchOne();
  • join 함수(join(), leftJoin(), rightJoin()) 뒤에 사용

서브 쿼리

eq(), goe(), in() 등의 함수에서 파라미터로 서브 쿼리 내용을 작성해주면 된다.
단, 서브 쿼리는 반드시 JPAExpressions로 시작해야한다.

//eq() 사용
queryFactory
    .selectFrom(member)
    .where(member.age.eq(
            JPAExpressions
                    .select(memberSub.age.max())
                    .from(memberSub)
    ))
    .fetch();

//goe() 사용
queryFactory
    .selectFrom(member)
    .where(member.age.goe(
            JPAExpressions
                    .select(memberSub.age.avg())
                    .from(memberSub)
    ))
    .fetch();

//in() 사용
queryFactory
    .selectFrom(member)
    .where(member.age.in(
            JPAExpressions
                    .select(memberSub.age)
                    .from(memberSub)
                    .where(memberSub.age.gt(10))
    ))
    .fetch();
  • select() 파라미터로도 들어갈 수 있고 방법은 같음
  • jpql이 from에서 서브쿼리를 지원하지 않아 querydsl도 지원하지 않음
    • 서브쿼리를 join으로 변경
    • 쿼리를 2개로 분리
    • 네이티브 쿼리 사용

Case문

when(), then()으로 처리할 수 있으며 마지막 케이스는 otherwise()로 처리한다.
복잡한 case문은 CaseBuilder를 사용해주자.

//단순 쿼리
queryFactory
    .select(member.age
            .when(10).then("열살")
            .when(20).then("스무살")
            .when(30).then("서른살")
            .otherwise("기타")
    )
    .from(member)
    .fetch();

//복잡한 쿼리
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();


상수나 문자 더하기

상수를 더할 땐 Expressions.constant()을, 문자를 더할 땐 concat()을 사용한다.

queryFactory
    .select(member.username, Expressions.constant("A"))
    .from(member)
    .fetch();

queryFactory
    .select(member.username.concat("_").concat(member.age.stringValue()))
    .from(member)
    .where(member.username.eq("member1"))
    .fetch();
  • member.age는 타입이 String이 아니기 때문에 concat() 파라미터로 쓸 수 없음
    • stringValue()로 타입을 String으로 변경

프로젝션

원하는 결과만 가져올 수 있도록 설정하는걸 프로젝션이라 한다.
대상이 1개면 그 대상의 타입을, 2개 이상이면 Tuple을 리턴한다.

List<String> result = queryFactory
                .select(member.username)
                .from(member)
                .fetch();

List<Tuple> result = queryFactory
                .select(member.username, member.age)
                .from(member)
                .fetch();
  • Tupleget()으로 참조 가능
    • get(member.username), get(member.age) 등으로 활용 가능

DTO 조회

MemberDto를 정의하고 querydsl에서 쿼리 결과가 Member가 아닌 바로 MemberDto로 얻는 방법을 알아보자.

@Data
@NoArgsConstructor
public class MemberDto {

    private String username;
    private int age;

    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setAge(int age) {
        this.age = age;
    }
}


querydsl에서는 3가지 방법으로 DTO를 생성할 수 있다.

//Setter
List<MemberDto> result = queryFactory
                .select(Projections.bean(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();

//필드 직접 접근
List<MemberDto> result = queryFactory
                .select(Projections.fields(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();

//생성자
List<MemberDto> result = queryFactory
                .select(Projections.constructor(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();
  • 차례대로 Setter 사용, 필드에 직접 접근, 생성자 활용을 통해 DTO를 얻고 있음

@QueryProjection를 사용해서 DTO를 편리하게 생성하는 방법이 있다.
MemberDto 생성자에 해당 어노테이션을 붙여줘야 한다.

//MemberDto
@QueryProjection
public MemberDto(String username, int age) {
    this.username = username;
    this.age = age;
}

//쿼리
List<MemberDto> result = queryFactory
                .select(new QMemberDto(member.username, member.age))
                .from(member)
                .fetch();
  • new QMemberDto()로 바로 DTO 생성 가능
  • 하지만 DTO가 @QueryProjection를 사용함으로써 querydsl에 의존한다는 점이 있음

동적 쿼리

동적 쿼리는 BooleanBuilder 또는 Where 다중 파라미터를 통해 해결할 수 있다.

private List<Member> useBooleanBuilder(String usernameParam, Integer ageParam) {
  BooleanBuilder builder = new BooleanBuilder();
  if(usernameParam != null) {
      builder.and(member.username.eq(usernameParam));
  }

  if(ageParam != null) {
      builder.and(member.age.eq(ageParam));
  }

  return queryFactory
          .selectFrom(member)
          .where(builder)
          .fetch();
}

private List<Member> whereMultipleParam(String usernameParam, Integer ageParam) {
  return queryFactory
          .selectFrom(member)
          .where(usernameEq(usernameParam), ageEq(ageParam))
          .fetch();
}

private Predicate usernameEq(String usernameParam) {
  if(usernameParam == null) {
      return null;
  }

  return member.username.eq(usernameParam);
}

private Predicate ageEq(Integer ageParam) {
  if(ageParam == null) {
      return null;
  }

  return member.age.eq(ageParam);
}
  • BooleanBuilder : 기준을 만들어서 where() 파라미터에 삽입하는 방식
  • Where 다중 파라미터 : where()null을 무시한다는 점을 이용한 방식
    • 파라미터가 null인 케이스에 한해서 null을 반환하고 있음
    • usernameEq(), ageEq() 참조

벌크 연산

업데이트 쿼리를 작성해주되 벌크 연산처럼 영속성 컨텍스트를 무시하고 바로 DB에 업데이트하는 경우 일관성이 깨질
수 있기 때문에 강제로 초기화를 해줘야 한다.

//예1 : 나이가 20 미만이면 이름을 비회원으로 변경
queryFactory
    .update(member)
    .set(member.username, "비회원")
    .where(member.age.lt(20))
    .execute();
em.flush();
em.clear();

//예2 : 모든 회원의 나이를 1만큼 올리기
queryFactory
    .update(member)
    .set(member.age, member.age.add(1))
    .execute();
em.flush();
em.clear();
  • execute()를 마지막에 호출
  • 영속성 컨텍스트 초기화 : em.flush(), em.clear()
  • 예2처럼 값 더할 땐 add() 사용하고, 곱할 땐 multiply() 사용

SQL function

SQL에서 쓰던 함수를 querydsl에서도 사용할 수 있다.
다음은 replace 함수lower 함수를 사용할 때 예시이다.

/**
 * 이름에서 member를 M으로 변경 
 */
List<String> result = queryFactory
                .select(Expressions.stringTemplate(
                        "function('replace', {0}, {1}, {2})",
                        member.username, "member", "M"))
                .from(member)
                .fetch();

/**
 * 이름을 소문자로 변경
 */ 
List<String> result = queryFactory
                .select(member.username)
                .from(member)
//                .where(member.username.eq(
//                        Expressions.stringTemplate("function('lower', {0})", member.username)))
                .where(member.username.eq(member.username.lower()))
                .fetch();
  • Expressions.stringTemplate()을 호출해 SQL function 사용
  • lower 함수처럼 ANSI 표준 함수는 querydsl에 내장돼있어 lower() 사용 가능

카테고리:

업데이트:

댓글남기기