Java

spring Querydsl

blackbearwow 2025. 5. 22. 19:18

1. 설정

1.1. build.gradle

설정은 스프링 버전에 따라 다르다. 이 글에서는 3.4.5버전에서의 설정이다.

dependencies {
	// QueryDsl
	implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
	annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

build.gradle에 위 설정을 추가한다. com.querydsl의 두가지를 추가하면 되는데, jakarta를 꼭 붙여야 정상 작동한다. 버전은 https://mvnrepository.com/artifact/com.querydsl에서 제일 상위 버전을 사용하면 된다.

 

2.1. Q클래스 빌드

의존성을 추가해 코끼리를 눌러 외부 라이브러리를 다 받아왔다면, Q클래스를 생성해줘야한다.

querydsl이 가지는 장점중 하나가 컴파일 때 타입 불인치와 같은 에러를 캐치하는것인데, Q클래스가 이것을 하기 위함이다.

Gradle에서 Tasks의 build에 들어가 clean을 더블클릭한 후 build를 더블클릭하면 된다.

그러면 프로젝트/build/generated에 JPA Entity인 클래스들이 Q클래스가 되어 생성되었다는것을 볼 수 있다.

파일 - 프로젝트 구조에서 build/generated를 소스 폴더로 지정한다. 그러면 이제 ide가 Q클래스를 정상 인식할 것이다.

 

위 과정을 진행해도 뭔가 오류가 걸릴 때가 있는데, 그러면 clean을 한 후 @SpringBootApplication을 실행시키면 src/main/generated/에 Q클래스가 생성된다. 그 후 코드를 작성하면 정상 작동된다.

 

그리고 .gitignore에 src/main/generated/를 추가하자. Q클래스가 해당 위치에 생길 때가 있는데, 깃에 올리지 말아야 하기 때문이다.

1.3. JPAQueryFactory 스프링 빈 등록

querydsl은 JPAQueryFactory 를 사용하기 때문에 스프링 빈으로 등록한다.

@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager em;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(em);
    }
}

이제 Repository에서 JPAQueryFactory를 주입받아 사용하면 된다.

 

2. 문법

2.1. select, from

queryFactory.select(member).from(member)
queryFactory.selectFrom(member)

select는 .select, from은 .from으로 사용하면 된다. 한꺼번에 하려면 selectFrom으로 하면 된다.

 

집계함수

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

 

프로젝션 대상이 하나라면 반환 타입을 명확하게 지정할 수 있다. 

프로젝션 대상이 여러개라면 튜플이나 DTO로 조회해야한다.

List<Integer> ageList = queryFactory.select(member.age).from(member).fetch();
List<Tuple> idAndAge = queryFactory.select(member.id, member.age).from(member).fetch();
// getter setter방식
List<IdAndNameDto> idAndName1 = queryFactory.select(Projections.bean(IdAndNameDto.class, member.id, member.name)).from(member).fetch();
// field 방식
List<IdAndNameDto> idAndName2 = queryFactory.select(Projections.fields(IdAndNameDto.class, member.id, member.name)).from(member).fetch();
// 생성자 방식
List<IdAndNameDto> idAndName3 = queryFactory.select(Projections.constructor(IdAndNameDto.class, member.id, member.name)).from(member).fetch();

2.2. join, on

queryFactory.selectFrom(cat)
    .innerJoin(cat.mate, mate)
    .leftJoin(cat.kittens, kitten)
    
queryFactory.selectFrom(cat)
    .leftJoin(cat.kittens, kitten)
    .on(kitten.bodyWeight.gt(10.0))

join은 from절 다음에 넣고 on도 사용할 수 있다.

 

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

엔티티에서 연관관계가 없는 세타조인은 위와 같이 할 수 있다.

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

세타조인인데, left조인이 필요할 경우. join에 엔티티 하나만 들어가야한다.

 

queryFactory.selectFrom(cat)
    .innerJoin(cat.mate, mate).fetchJoin()
    .leftJoin(cat.kittens, kitten)

fetch join으로 즉시로딩이 필요할 경우, join절 뒤에 .fetchJoin()을 붙여주면 된다.

2.3. where, and, or

queryFactory.selectFrom(member).where(member.name.eq("choi")).fetchOne();
queryFactory.selectFrom(member).where(member.name.contains("choi")).fetchOne();

간단한 사용 예제이다.

queryFactory.selectFrom(customer)
    .where(customer.firstName.eq("Bob"), customer.lastName.eq("Wilson"));
queryFactory.selectFrom(customer)
    .where(customer.firstName.eq("Bob").and(customer.lastName.eq("Wilson")));

위는 둘다 같은 and절의 사용 예제이다.

queryFactory.selectFrom(customer)
    .where(customer.firstName.eq("Bob").or(customer.lastName.eq("Wilson")));

or사용 예제이다.

 

검색 조건 메소드

메소드 설명
eq() equal. 같은지
ne() not equal. 같지 않은지
isNotNull() null이 아닌지
in() 명시된 값에 포함되는지
notIn() 명시된 값에 포함되지 않는지
between() 명시된 값들 사이인지
goe() greater or equal. 같거나 크다
gt() greater than. 명시된 값 초과
loe() little or equal.
lt() little than. 명시된 값 미만
like() like검색
contains() like %search% 검색
startsWith() like search% 검색

 

querydsl의 꽃인 동적 쿼리는 두가지 방법으로 할 수 있다. 

첫번째는 BooleanBuilder를 사용하는 방법이다.

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

public List<Member> searchMember2(String name, Integer age) {
    BooleanBuilder builder = new BooleanBuilder();
    if (name != null) {
        builder.and(member.name.eq(name));
    }
    if (age != null) {
        builder.and(member.age.eq(age));
    }
    BooleanBuilder builder1 = new BooleanBuilder();
    return queryFactory.selectFrom(member).where(builder.or(builder1)).fetch();
}

builder에 and나 or를 사용해 조건을 추가할 수 있고, builder마다 or조건으로 이을수도 있다.

 

두번째는 where 다중 파라미터 방식이다.

public List<Member> searchMember(String name, Integer age) {
    return queryFactory.selectFrom(member).where(nameEq(name), ageEq(age)).fetch();
}
private BooleanExpression nameEq(String name) {
    return name == null ? null : member.name.eq(name);
}
private BooleanExpression ageEq(Integer age) {
    return age == null ? null : member.age.eq(age);
}

 

2.4. order by

queryFactory.selectFrom(customer)
    .orderBy(customer.lastName.asc(), customer.firstName.desc())
    .fetch();

orderBy메소드에 넣고싶은 정렬을 순서대로 넣으면 된다.

queryFactory.selectFrom(customer)
    .orderBy(customer.lastName.asc().nullsFirst())
    .fetch();

nullsFirst()나 nullsLast()를 써서 널값을 정렬 앞에 올지 뒤에 올지 정할 수 있다.

2.5. group by, having

queryFactory.select(customer.lastName).from(customer)
    .groupBy(customer.lastName)
    .fetch();
    
queryFactory.select(team.name, member.age.avg())
    .from(member)
    .join(member.team, team)
    .groupBy(team.name)
    .fetch()
    
queryFactory.select(customer.lastName).from(customer)
    .groupBy(customer.lastName)
    .having(customer.age.gt(20))
    .fetch();

 

 

2.6. subquery

서브쿼리에서는 queryFactory대신에 JPAExpressions를 사용해 서브쿼리에 사용한다.

QMember mem2 = new QMember("mem2");
queryFactory.selectFrom(member)
        .where(member.age.goe(
                JPAExpressions.select(mem2.age.avg()).from(mem2)
        )).fetch();

 

jpa는 select와 where절에서 서브쿼리가 되지만, from절에서는 지원하지 않는다.

 

from절에서 서브쿼리를 써야하는 상황이라면 

1. 서브쿼리를 join으로 변경한다. (불가능한 경우도 있다)

2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.

3. native SQL을 사용한다.

2.7. offset, limit (페이징)

queryFactory.selectFrom(member).offset(10).limit(10).fetch();

offset으로 몇번째부터 가져올지 정하고 (0부터 시작)

limit으로 몇개를 가져올지 정한다.

2.8. 결과 조회

메소드 설명
fetch() 리스트 조회, 데이터 없으면 빈 리스트 반환
fetchOne() 단 한건 조회, 결과가 없으면 null
결과가 둘 이상이면 NonUniqueResultException발생
fetchFirst() limit(1)을 한후 fetchOne()한것과 같다.

fetchCount()와 fetchResults()는 deprecared되었다.

2.9. 수정, 삭제 벌크 연산

벌크 연산 후에는 영속성 컨텍스트와 db의 값이 달라지므로, 영속성 컨텍스트를 초기화 해야한다.

 

-업데이트: update, set, execute메소드를 사용하면 된다.

long count = queryFactory
        .update(member)
        .set(member.age, 30)
        .where(member.age.lt(30))
        .execute();
        
em.flush();
em.clear();

- 삭제: delete, execute메소드를 사용하면 된다.

long count = queryFactory
        .delete(member)
        .where(member.age.lt(30))
        .execute();

 

 

spring 데이터 jpa를 공부 후, querydsl과 함께 쓰는 방법을 정리하자.


참고: https://velog.io/@kimsundae/Gradle-SpringBoot-3.x-QueryDSL-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0

http://querydsl.com/static/querydsl/5.0.0/reference/html_single/#jpa_integration

-