JVM/JPA

QueryDSL

kyoulho 2023. 6. 28. 19:56

build.gradle

dependencies {
    ...
    // QueryDSL JPA 라이브러리
    implementation 'com.querydsl:querydsl-jpa' 
    // QueryDSL 관련된 쿼리 타입(QClass)을 생성할 때 필요한 라이브러리로, annotationProcessor을 사용하여 추가
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
    // java.lang.NoClassDefFoundError(javax.annotation.Entity) 발생 시 추가
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
    // java.lang.NoClassDefFoundError(javax.annotation.Generated) 발생 시 추가
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
}

test {
    useJUnitPlatform()
}

// 빌드시 QClass 소스도 함께 빌드하기 위해서 sourceSets에 해당 위치를 추가해준다.
def querydslSrcDir = 'src/main/generated'
sourceSets {
   main.java.srcDir querydslSrcDir
}

// annotaion processors 에서 생성한 소스 파일을 저장할 디렉토리를 지정해 준다.
tasks.withType(JavaCompile) {
    options.generatedSourceOutputDirectory = file(querydslSrcDir)
}

// clean 시 쿼리 타입 삭제
clean {
    delete file(querydslSrcDir)
}

annotationProcessor는 자바 컴파일러 플러그인 일종으로, 컴파일 단계에서 프로젝트 내의 @Entity 애노테이션을 선언한 클래스를 탐색하여 com.querydsl.apt.jpa.JPAAnnotationProcessor를 통해 쿼리 타입(QClass)을 생성한다.

생성된 쿼리 타입은 com.querydsl.core.QueryFactory에 주입하여 사용한다.

 

기본 사용법

QMember m = new QMember("m");
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
Member findMember = queryFactory
        .select(m)
        .from(m)
        .where(m.username.eq("member1"))
        .fetchOne();

 

기본 Q 생성

쿼리 타입은 사용하기 편리하도록 기본 인스턴스를 보관하고 있다. 하지만 같은 엔티티를 조인하거나 서브쿼리에 사용하면 같은 별칭이 사용되므로 이때는 별칭을 직접 지정해서 사용해야 한다.

서브 쿼리를 사용할 때 외에는 기본 인스턴스르 static import와 함께 사용하는 것을 권장한다.

별칭 지정
QMember qMember = new QMember("m");

기본 인스턴스 사용
QMember qMember = QMember.member;

 

검색 조건 쿼리

검색 조건은 메서드 체인으로 연결할 수 있고 모든 검색 조건을 제공하고 있다.

함수 SQL
eq(1)  == 1
ne(1)  != 1
eq(1).not()  != 1
isNotNull() is not null
in(10,20) in (10,20)
notIn(10,20) not in (10,20)
between(10,30) between 10, 30
goe(30)  >= 30
ge(30)  > 30
loe(30)  <= 30
lt(30)  < 30
like("member")  like 'member'
contains("member")  like '%member%'
startWith("member")  like 'member%'

 

결과 조회 함수

함수 설명
fetch() 리스트 조회, 데이터 없으면 빈 리스트
fetchOne() 단건 조회, 결과 없으면 null, 둘 이상이면 com.querydsl.coreNonUniqueResultException
fetchFirst() limit(1).fetchOne()
fetchResults() Deprecated
fetchCount() Deprecated

 

정렬과 페이징

전체 데이터 수를 함께 조회하는 함수는 Deprecated 되었다.

count 전용 쿼리를 별도로 작성해야 한다.

함수 설명
desc(), asc() 일반 정렬
nullsLast(), nullsFirst() null 데이터 순서 부여
offset().limit() 시작 위치, 개수
restrict(QueryModifiers(limit,offset)) QueryModifiers 객체 사용

 

GroupBy, Having

queryFactory.from(member).groupBy(member.age).having(member.age.gt(20)).fetch();

 

조인

조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭으로 사용할 Q타입을 지정한다.

 

기본 조인

QMember member = QMember.member;
QTeam team = QTeam.team;
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
List<Member> result = queryFactory.selectFrom(member)
        .join(member.team, team)
        .where(team.name.eq("TeamA"))
        .fetch();

 

세타 조인

em.persist(new Member("teamA"));
em.persist(new Member("teamB"));

List<Member> result = queryFactory
        .select(member)
        .from(member,team) // from 절에 여러 엔티티를 선택해서 세타 조인한다.
        .where(member.username.eq(team.name))
        .fetch();
        
// SQL
select
    member0_.member_id as member_i1_0,
    member0_.age as age2_0_,
    member0_.team_id as team_id4_0_,
    member0_.username as username3_0_ 
from
    member member0_ cross 
join
    team team1_ 
where
    member0_.username=team1_.name

 

조인 on 절

일반 조인 : leftJoin(member.team, team)

on 조인 : from(member).leftJoin(team).on(조건식)

List<Tuple> result = queryFactory
        .select(member, team)
        .from(member)
        .leftJoin(member.team, team).on(team.name.eq("teamA"))
        .fetch();
        
// SQL        
select
    member0_.member_id as member_i1_0_0_,
    team1_.team_id as team_id1_1_1_,
    member0_.age as age2_0_0_,
    member0_.team_id as team_id4_0_0_,
    member0_.username as username3_0_0_,
    team1_.name as name2_1_1_ 
from
    member member0_ 
left outer join
    team team1_ 
        on member0_.team_id=team1_.team_id 
        and (
            team1_.name=?
        )

// 연관관계 없는 외부 조인        
List<Tuple> result = queryFactory
                    .select(member,team)
                    .from(member)
                    .leftJoin(team).on(member.username.eq(team.name))
                    .fetch();
// SQL           
select
    member0_.member_id as member_i1_0_0_,
    team1_.team_id as team_id1_1_1_,
    member0_.age as age2_0_0_,
    member0_.team_id as team_id4_0_0_,
    member0_.username as username3_0_0_,
    team1_.name as name2_1_1_ 
from
    member member0_ 
left outer join
    team team1_ 
        on (
            member0_.username=team1_.name
        )

 

페치 조인

즉시로딩으로 한번에 조인한다. 조인 뒤에 fetchJoin()이라고 추가하면 된다.

Member findMember = queryFactory
            .selectFrom(member)
            .join(member.team,team).fetchJoin()
            .where(member.username.eq("member1"))
            .fetchOne();
            
 // SQL
 select
    member0_.member_id as member_i1_0_0_,
    team1_.team_id as team_id1_1_1_,
    member0_.age as age2_0_0_,
    member0_.team_id as team_id4_0_0_,
    member0_.username as username3_0_0_,
    team1_.name as name2_1_1_ 
from
    member member0_ 
inner join
    team team1_ 
        on member0_.team_id=team1_.team_id 
where
    member0_.username=?

 

서브 쿼리

com.querydsl.jpa.JPAExpressions 객체를 이용한다.

FROM 절의 서브쿼리는 지원하지 않는다.

QMember memberSub = new QMember("memberSub"); // 별칭

List<Member> result = queryFactory
        .selectFrom(member)
        .where(member.age.eq(
                JPAExpressions
                        .select(memberSub.age.max())
                        .from(memberSub)
        )).fetch();

 

Case 문

void basicCase() {
    List<String> result = queryFactory
            .select(member.age
                    .when(10).then("10")
                    .when(20).then("20")
                    .otherwise("00")
            ).from(member)
            .fetch();
}

// SQL
 select
    case 
        when member0_.age=? then ? 
        when member0_.age=? then ? 
        else '00' 
    end as col_0_0_ 
from
    member member0_

void complexCase() {
    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("00")
            ).from(member)
            .fetch();
}

// SQL
select
    case 
        when member0_.age between ? and ? then ? 
        when member0_.age between ? and ? then ? 
        else '00' 
    end as col_0_0_ 
from
    member member0_

 

상수 문자 더하기

void constant() {
    Member member1 = new Member();
    member1.setUsername("kyun");
    em.persist(member1);

    List<Tuple> result = queryFactory
            .select(member.username, Expressions.constant("A"))
            .from(member)
            .fetch();

    for (Tuple tuple : result) {
        System.err.println(tuple);
    }
}

// 결과 [kyun, A]

void concat() {
    Member member1 = new Member();
    member1.setUsername("kyun");
    member1.setAge(100);
    em.persist(member1);
    
    List<String> result = queryFactory
            .select(member.username.concat("_").concat(member.age.stringValue()))
            .from(member)
            .where(member.username.eq("kyun"))
            .fetch();
}

// 결과 s = kyun_100

 

DTO 결과 반환

// bean()
// getter, setter, NoArgConstructor 필요
List<MemberDto> result = queryFactory
                          .select(Projections.bean(MemberDto.class,
                                        member.username,
                                        member.age))
                          .from(member)
                          .fetch();

// fields()
// 바로 주입, getter, setter 필요 없음
List<MemberDto> result = queryFactory
                        .select(Projections.fields(MemberDto.class,
                                    member.username,
                                    member.age))
                        .from(member)
                        .fetch();    
// constructor()
// 생성자 필요
List<MemberDto> result = queryFactory
                            .select(Projections.constructor(MemberDto.class,
                                      member.username,
                                      member.age))
                            .from(member)
                            .fetch();

// DTO 필드명이 다를 때                            
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();

 

@QueryProjection

DTO 생성자에 붙여주면 DTO도 Q파일로 생성된다.

컴파일 시점에 오류를 잡아낼 수 있는 장점이 있지만 DTO가 QueryDSL에 대한 의존성이 생긴다는 단점이 있다. 

import com.querydsl.core.annotations.QueryProjection;

public class MemberDto {

    private String username;
    private int age;

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

queryFactory
    .select(new QMemberDto(member.username, member.age))
    .from(member)
    .fetch();

 

수정, 삭제 배치 쿼리(벌크 연산)

JPQL 배치 쿼리와 같이 영속성 컨텍스트를 무시하고 데이터베이스를 직접 쿼리한다. 

따라서 배치 쿼리 이후 같은 트랜잭션에서 다시 조회할 경우에는 em.flush()와 em.clear()를 해줘야 한다.

public void bulkUpdate() {
    long count = queryFactory
            .update(member)
            .set(member.username, "비회원")
            .where(member.age.lt(28))
            .execute();
}

 

동적 쿼리

BooleanBuilder

private List<Member> searchMember(String nameCond, Integer ageCond) {
    BooleanBuilder builder = new BooleanBuilder();
    
    if(nameCond != null) {
        builder.and(member.name.eq(nameCond));
    }
    if(ageCond != null) {
        builder.and(member.age.eq(ageCond));
    
    return queryFactory
            .selectFrom(member)
            .where(builder)
            .fetch();
}

 

BooleanExpression

private List<Member> searchMember(String nameCond, Integer ageCond) {
    return queryFactory
            .selectFrom(member)
            .where(nameEq(nameCond), ageEq(ageCond))
            .fetch();
}

private BooleanExpression nameEq(String nameCond) {
    if (nameCond == null) {
        return null;
    }
    return member.name.eq(nameCond);
}

private BooleanExpression ageEq(Integer ageCond) {
    if (ageCond == null) {
        return null;
    }
    return member.age.eq(ageCond);
}

 

함께 사용

// query
private List<Post> search(final Map<String, String> searchCondition) {
    return queryFactory
            .selectfrom(post)
            .where(allCond(searchCondition))
            .fetch();
}

// BooleanBuilder
private BooleanBuilder allCond(final Map<String, String> searchCondition) {
    BooleanBuilder builder = new BooleanBuilder();

    return builder
            .and(titleLike(searchCondition.getOrDefault(TITLE.getParamKey(), null)))
            .and(categoryEq(searchCondition.getOrDefault(CATEGORY.getParamKey(), null)))
            .and(tagLike(searchCondition.getOrDefault(TAG.getParamKey(), null)))
            .and(stateEq(searchCondition.getOrDefault(STATE_CODE.getParamKey(), null)))
            .and(withInDays(searchCondition.getOrDefault(DAYS_AGO.getParamKey(), null)));
}

// 조건1
private BooleanExpression titleLike(final String title) {
    return StringUtils.hasText(title) ? post.title.contains(title) : null;
}

// 조건2
private BooleanExpression categoryEq(final String category) {
    if (!StringUtils.hasText(category)) return null;

    Category instance = Category.getInstance(category);
    return post.category.eq(instance);
}

// 조건3
private BooleanExpression titleLike(final String tag) {
    return StringUtils.hasText(tag) ? post.tag.contains(tag) : null;
}

// 조건4
private BooleanExpression stateEq(final String stateCode) {
    if (!StringUtils.hasText(stateCode)) return null;

    Integer code = convertInteger(stateCode);
    RecruitStatus instance = RecruitStatus.getInstance(code);
    return post.status.eq(instance);
}

// 조건5
private BooleanExpression withInDays(final String daysAgo) {
    if (!StringUtils.hasText(daysAgo)) return null;

    Long days = convertLong(daysAgo);
    LocalDate now = LocalDate.now();
    LocalDate startDate = now.minusDays(days);
    LocalDateTime startDateTime = LocalDateTime.of(startDate, LocalTime.MIN);

    return post.createdDate.goe(startDateTime);
}

 

메소드 위임

쿼리 타입에 검색 조건을 직접 정의할 수 있다. WHERE 절이 깔끔해진다.

QClass를 컴파일해야 한다.

@QueryEntity
public class MemberExpression {
    @QueryDelegate(Member.class)
    public static BooleanExpression isOlder(QMember member, Integer age) {
        return member.age.gt(age);
    }
}

// QMember
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QMember extends EntityPathBase<Member> {
	...
	public BooleanExpression isOlder(Integer age) {
    	    return MemberExpression.isOlder(this, age);
	}
    ...
}

// 사용
List<Member> fetch = queryFactory.selectFrom(member)
         .where(member.isOlder(10))
         .fetch();

'JVM > JPA' 카테고리의 다른 글

스토어드 프로시저(JPA 2.1)  (0) 2023.06.30
네이티브 SQL  (0) 2023.06.30
JPQL 다형성 쿼리, 사용자 정의 함수, Named 쿼리  (0) 2023.06.28
JPQL 조건식  (0) 2023.06.28
JPQL 서브쿼리  (0) 2023.06.26