QueryDSL Part.1

dev_314·2023년 4월 3일
0

JPA - Trial and Error

목록 보기
14/16

QueryDSL 설정

스프링 부트 2.6부터 Querydsl 5.0을 사용한다.

buildscript {
    ext {
        queryDslVersion = "5.0.0"
    }
}

plugins {
	...
    //querydsl 추가
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
...
dependencies {
    //querydsl 추가
    implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
    annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}"
    ...
}
...
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
sourceSets {
    main.java.srcDir querydslDir
}
configurations {
    querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}


compileQuerydsl을 클릭하면 @Entity Annotation이 붙은 Class를 확인하면서 Qxxx라는 이름의 클래스를 생성한다.
즉, Member Entity가 존재하면 QMember라는 클래스가 생기는 것이다.
QueryDSL은 Entity를 한 번 감싼(?) Q클래스를 통해 데이터에 접근하는 것이다.


querydsl-apt: QClass를 만드는 역할
querydsl-jpa: 쿼리 작성 역할

JPQL vs QueryDSL

유저 이름으로 조회하기

    @Test
    void jqplTest() {
        String query = "SELECT m FROM Member m WHERE m.username = :username";
        Member findMember = em.createQuery(query, Member.class)
                .setParameter("username", "username#1")
                .getSingleResult();
        assertThat(findMember.getUsername()).isEqualTo("username#1");
    }

    @Test
    void queryDslTest() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m = new QMember("m");
        Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq("username#1"))
                .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo("username#1");
    }
  1. JQPL은 문자열을 다루는 것 과 달리, QueryDSL은 쿼리를 자바 코드를 사용하는 것 처럼 작성한다.
    • 쿼리를 잘못 작성할 걱정이 없다.
    • 파라미터 바인딩을 알아서 해준다.
  2. QueryDSL은 컴파일 타임에 쿼리 문법을 검사한다.
    • 런타임에 쿼리 문법 예외가 발생할 일이 없다.

권장

    @Test
    void queryDslTest() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m = new QMember("m");
        Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq("username#1"))
                .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo("username#1");
    }

Spring이 주입하는 EntityManager는 Thread Safe하기 때문에, JPAQueryFactory를 필드로 추출해서 사용해도 된다.

	JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    @Test
    void queryDslTest() {
        QMember m = new QMember("m");
        Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq("username#1"))
                .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo("username#1");
    }

기본 문법

Q-Type

QueryDSL은 Q클래스 인스턴스를 통해 데이터에 접근한다.

Q클래스인스턴스는 두 가지 방법으로 사용할 수 있다.

// 1. 별칭 직접 입력
// 입력한 별칭이 SQL문 Table의 별칭으로 사용된다.
// SELECT m1.username FROM Member AS m1
QMember qMember = new QMember("m1");
// 2. 기본 인스턴스 사용
// `Q클래스` 내부적으로 미리 초기화 해놓은 인스턴스를 가지고 있다.
QMember qMember = QMember.member;

2. 기본 인스턴스 사용 사용을 권장한다.

// 1. 기본 사용 
QMember qMember = QMember.member;
// 2. Static Import로 간소화 하기
import static 경로.entity.QMember.*;
QMember member = member; // QMember.member를 간소화

같은 Table을 다룰 때는 Table을 구분해야 하므로, 그때 Alias 방식을 사용한다.

검색 조건 쿼리

qf // queryFactory
.select(member)
.from(member)
.where(
	member.username.eq("a") // username = "a"
    member.username.ne("a") // username != "a"
    member.username.eq("a").not // username != "a"
    member.username.isNotNull() // username is not null
    
    member.age.in(10, 20) // age IN (10, 20)
    member.age.notIn(10, 20) age NOT IN (10, 20)
    member.age.between(10, 20) age BETWEEN 10 AND 20;
    
    member.age.goe(30) // age >= 30 (greater or equal)
    member.age.gt(30) // age > 30
    member.age.loe(30) // age <= 30 (less or equal)
    member.age.lt(30) // age < 30
    
    member.username.like("member%") // like member%
    member.username.contains("member") // like %member%
    member.username.startsWith("member") // like member%
).fetchOne();

검색 조건 쿼리 - And

두 방식으로 And 조건을 설정할 수 있다.

// 1. and() 사용
qf
.select(member)
.from(member)
.where(member.username.eq("a").and(member.age.eq(10)))
// 2. 파라미터로 사용
qf.select(member)
.from(member)
.where(
	member.username.eq("a"),
    member.age.eq(10)
)

2. 파라미터로 사용을 권장함

  • 가독성
  • 파라미터에 null이 들어가면 알아서 무시함
    • 동적 쿼리 작성할 때 편하다.

결과 조회

// 1. List로 조회
// selectFrom : Entity가 같은 경우 축약 가능 (select + from)
List<Member> members = qf.selectFrom(member).fetch();
// 2. 단건 조회
Member member = qf.selectFrom(member).fetchOne();
// 3. 첫번째 결과만 조회
Member member = qf.selectFrom(member).fetchFirst();
// 4. 복합 결과 (페이징)
// 페이징을 위해 count 개수를 검색하는 쿼리가 발생함 (총 쿼리 2번)
// 최신 버전에서는 deprecated
QueryResults<Member> results = qf.selectFrom(member).fetchResults();
// 5. 데이터 개수 조회 (count)
long total = qf.selectFrom(member).fetchCount();

OrderBy

나이 기준 오름차순, 이름 기준 내림차순, 이름이 null인 경우 뒤에 위치하도록 정렬
List<Member> fetch = qf
	.selectFrom(member)
	.orderBy(member.age.asc(), member.username.desc().nullsLast())
.fetch();

nullsLast: 값이 null인 경우 마지막에 위치
nullsFirst: 값이 null인 경우 앞에 위치

페이징

// 1페이지에서부터 3개 불러오기
List<Member> results = qf
	.selectFrom(member)
	.offset(1).limit(3).fetch();

QueryDSL도 0부터 페이지가 시작한다.

집합

Aggregate Function

List<Tuple> result = qf.select(
		member.count(),
		member.age.sum(),
		member.age.avg(),
		member.age.max(),
		member.age.min()
		)
		.from(member)
		.fetch();
Tuple tuple = result.get(0);
Long count = tuple.get(member.count());
Integer sum = tuple.get(member.age.sum());
Double avg = tuple.get(member.age.avg());
Integer max = tuple.get(member.age.max());
Integer min = tuple.get(member.age.min());

Tuple은 QueryDSL이 제공하는 자료구조이다.

Group By, Having

팀 인원이 10명 이상인 팀의 이름과, 평균 나이
List<Tuple> result = qf
		.select(team.name, member.age.avg())
		.from(member)
		.join(member.team, team)
		.groupBy(team.name)
        .having(team.count().goe(10))
		.fetch();
Tuple team1 = result.get(0);
Tuple team2 = result.get(2);

Join

List<Member> result = qf
		.select(member)
        .from(member)
        // join, innerJoin, leftJoin, rightJoin
        // join을 수행하면 JPA를 따라 Inner Join 발생
        .join(member.team, team) // QTeam.team
        .fetch();

첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터 alias로 사용할 Q타입을 지정하면 된다.

Join - On

  • JPA 2.1부터 On을 사용할 수 있다.

    	회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인, 회원은 모두 조회
    	(JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t ON t.name = 'A')
List<Tuple> result = qf
		.selectFrom(member, team)
		.from(member)
		.leftJoin(member.team, team).on(team.name.eq("A"))
		.fetch();

Outer Join이므로, 팀 A에 속하지 않는 멤버 결과가 포함된다. null에 주의해야 한다.

Inner Join을 사용할 때 필터링 조건을 On에 걸든, Where절에 걸든 결과는 동일하다.
따라서 일관된 스타일을 위해서, Inner Join에서는 익숙한 Where절을 사용하고, Outer Join에 경우에만 On절을 사용하도록 하자.

Join의 대상

join을 사용할 때 두 문법이 있다.

// 1. 일반 조인
qf
.select(member)
.from(member)
.join(member.team, team)
.fetch();
// 2. 조인대상만 사용 + On 절
qf
.select(member)
.from(member)
.join(team).on(member.team.id.eq(team.id))
.fetch()

첫 번째 방식처럼 사용하면 알아서 PK, FK 기준으로 ON절을 구성한다.
즉, SQL ON절에 member.team_id = team.id가 추가된다.

두 번째 방식처럼 사용하면 알아서 PK, FK 기준으로 ON절을 구성하지 않는다.
그러므로 명시적으로 ON절을 설정해 줘야 한다.

연관 관계가 없는 경우

Cross Join (Cartesian Product): N * M

List<Tuple> tuples = qf
		.select(memeber, team)
		.from(member, team)
		.fetch();
// JPQL
SELECT member1, team FROM Member member1, Team team

Theta Join: Caretsian Product에서 특정 조건 (세타 조건)을 만족하는 경우만 반환

유저 이름과 팀 이름이 같은 경우
List<Tuple> result = qf
		.select(member)
        .from(member, team)
        .where(member.username.eq(team.name))
        .fetch();
// JQPL
SELECT member1, team FROM Member member1, Team team WHERE member1.username = team.name

// 그냥 연습용으로 비교
List<Member> members = qf
		.select(member)
		.from(member)
        .where(member.username.eq(member.team.name))
		.fetch();
// JPQL
// 묵시적 JOIN 발생
SELECT member1 FROM Member member1 WHERE member1.username = member1.team.name

Fetch Join

// QueryDSL
List<Member> result = qf
		.select(member)
		.from(member)
		.join(member.team, team).fetchJoin()
		.where(team.name.eq("team1"))
		.fetch();
// JPQL
SELECT m FROM Member m JOIN FETCH m.team t WHERE t.name = "team1"

어떤 Join이든지 fetchJoin()을 같이 사용하면 된다.

fetchJoin을 사용하지 않아도 같은 효과를 낼 수 있다.

// QueryDSL
List<Tuple> team1 = qf
		.select(member, member.team)
		.from(member)
		.join(member.team, team)
		.where(team.name.eq("team1"))
		.fetch();

다만 반환 타입이 Tuple이다.

Subquery

com.querydsl.jpa.JPAExpressions를 사용하여 서브쿼리를 구성할 수 있다.

// 가장 나이가 많은 회원 조회
QMember subQMember = new QMember("sub_member");
List<Member> members = qf
		.selectFrom(member)
		.where(member.age.eq(
			JPAExpressions
		    		.select(subQMember.age.max())
		            .from(subQMember)
		))
		.fetch();

초반부에 다뤘던 것 처럼, 같은 Table(Entity)를 다루므로 Alias를 다르게 해서 구분해야 한다.

// SELECT절 Subquery
QMember subQMember = new QMember("sub_member");
List<Member> members = qf
		.select(
			member,username,
			JPAExpressions
			    .select(subQMember.age.avg())
		        .from(subQMember))
		.from(member)
		.fetch();

역시나 JPAExpressions도 static import로 분리하여 가독성을 높일 수 있다.

JPA, JPQL이 From절 Subquery를 지원하지 않으므로, QueryDSL도 불가능하다.
다음의 방법으로 해결 할 수 있을 것이다.

1. (가능한 경우) JOIN으로 변경하기
2. 쿼리를 두 번 날려서 변경하기
3. NativeSQL 사용하기

SELECT절 Subquery는 JPA는 불가능한데, Hibernate는 가능해서 QueryDSL도 가능한 것이다.

Case

//Simple CASE Statement
List<String> ages = qf
		.select(member.age
				.when(10).then("ten")
		        .when(20).then("twenty")
		        .otherwise("??"))
		.from(member)
		.fetch()
// Searched CASE Statement
List<String> ages = qf
		.select(new CaseBuilder()
				.when(member.age.between(0,10)).then("ten")
                .when(member.age.between(11,20)).then("twenty")
				.otherwize("??"))
		.from(member)
		.fetch();

DB에서는 단순히 데이터만 조회하기를 권장한다.

상수, 문자 더하기

// 상수 (Expressions)
List<Tuple> result = qf
				.select(member.username, Expressions.constant("A"))
                .from(member)
                .fetch();
// 문자 더하기 (concat)
List<String> result = qf
				.select(member.username.concat("_").concat(member.age.stringValue()))
                .from(member)
                .fetch();

상수는 JPQL, SQL에는 반영되지 않고, 최종 결과에서만 반영된다.
문자 더하기는 JPQL, SQL에도 반영되서 전달된다.

profile
블로그 이전했습니다 https://dev314.tistory.com/

0개의 댓글