[SQL] QueryDSL

๊น€์ •๋ฏผยท2024๋…„ 4์›” 17์ผ
1
post-thumbnail

JPA๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๊ตฌํ˜„ํ•  ์ˆ˜ ์—†๋Š” ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋ฅผ ๊ทน๋ณตํ•  ์ˆ˜ ์žˆ๋Š” QueryDSL์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž.

๋จผ์ € ๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค๋ช…ํ•˜์ž๋ฉด QueryDSL์€ ์ฟผ๋ฆฌ๋ฅผ ์ž๋ฐ”์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•˜์—ฌ ์žฅ์ ์„ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

๋” ์ž์„ธํ•˜๊ฒŒ ์•Œ์•„๋ณด์ž.

๐Ÿ’ก QueryDSL ์ด๋ž€?

์ •์  ํƒ€์ž…์„ ์ด์šฉํ•œย Query์ƒ์„ฑ์— ํŠนํ™”๋œ ์–ธ์–ด(Domain Specific Language)์˜ ํŠน์ง•์„ ๊ฐ–๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค

๐Ÿ”‘ QueryDSL์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ 

1. ์ฟผ๋ฆฌ๋ฅผ ์ž๋ฐ”์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•˜์—ฌ ์ปดํŒŒ์ผ ์—๋Ÿฌ๋กœ ์‹ค์ˆ˜๋ฅผ ์žก์„ ์ˆ˜ ์žˆ๋‹ค.

2. ๊ฐ€๋…์„ฑ์ด ๋†’๊ณ  ๋™์ ์ฟผ๋ฆฌ ์ž‘์„ฑํ•˜๊ธฐ ํŽธ๋ฆฌํ•˜๋‹ค.

3. DB ์ข…๋ฅ˜๊ฐ€ ๋‹ฌ๋ผ์ ธ๋„ ์ฟผ๋ฆฌ๋ฅผ ๋‹ค์‹œ ์ž‘์„ฑํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.

์œ„ 3๊ฐ€์ง€ ์žฅ์ ์ด ๊ฐ•๋ ฅํ•˜์—ฌ QueryDSL์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ์ถฉ๋ถ„ํ•œ ์ด์œ ๊ฐ€ ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

๐Ÿ› ๏ธ Gradle ์„ค์ •

spring boot 3.x

plugins {
   id 'java'
   id 'org.springframework.boot' version '3.2.0'
   id 'io.spring.dependency-management' version '1.1.4'
}


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'
   implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
   compileOnly 'org.projectlombok:lombok'
   runtimeOnly 'com.h2database:h2'
   annotationProcessor 'org.projectlombok:lombok'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'


   //test ๋กฌ๋ณต ์‚ฌ์šฉ
   testCompileOnly 'org.projectlombok:lombok'
   testAnnotationProcessor 'org.projectlombok:lombok'


   //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()
}


clean {
   delete file('src/main/generated')
}

spring boot 2.x

plugins {
	id 'org.springframework.boot' version '2.6.5'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

ext["hibernate.version"] = "5.6.5.Final"

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'

	//JdbcTemplate ์ถ”๊ฐ€
	//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	//MyBatis ์ถ”๊ฐ€
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
	//JPA, ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA ์ถ”๊ฐ€
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

	//Querydsl ์ถ”๊ฐ€
	implementation 'com.querydsl:querydsl-jpa'
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"

	//H2 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ถ”๊ฐ€
	runtimeOnly 'com.h2database:h2'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	//ํ…Œ์ŠคํŠธ์—์„œ lombok ์‚ฌ์šฉ
	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
}

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

//Querydsl ์ถ”๊ฐ€, ์ž๋™ ์ƒ์„ฑ๋œ Qํด๋ž˜์Šค gradle clean์œผ๋กœ ์ œ๊ฑฐ
clean {
	delete file('src/main/generated')
}

๐Ÿšช๊ธฐ๋ณธ ๋ฌธ๋ฒ•

  private final JPAQueryFactory queryFactory;

  public MemberRepositoryImpl(EntityManager em) {
    this.queryFactory = new JPAQueryFactory(em);
  }

๊ธฐ๋ณธ์ ์ธ ์กฐํšŒ

Member findMember = queryFactory
                .select(member)
                .from(member)
                .where(member.username.eq("member1"))
                .fetchOne();
  • QueryDSL์„ ์ด์šฉํ•œ ๊ธฐ๋ณธ์ ์ธ where์ ˆ์„ ์ด์šฉํ•œ ์กฐํšŒ์ด๋‹ค.
  • QMember์™€ ๊ฐ™์ด QType ํด๋ž˜์Šค๋ฅผ static importํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ„๊ฒฐํ•œ ์ฝ”๋“œ๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๋ณ„์นญ์„ ๋‹ค๋ฅด๊ฒŒ ๋‘์–ด์•ผ ํ•œ๋‹ค๋ฉด ์ƒˆ๋กœ ์„ ์–ธํ•˜์ž.

๊ฒ€์ƒ‰์กฐ๊ฑด

Member findMember = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1").and(member.age.between(10, 30)))
                .fetchOne();
  • where์ ˆ์—๋Š” and๋‚˜ or์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • and ์—ฐ์‚ฐ์˜ ๊ฒฝ์šฐย .where(member.username.eq("member1"), member.age.between(10, 30))์™€ ๊ฐ™์ดย ,๋ฅผ ์ด์šฉํ•ด ๊ฐ„๋‹จํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
    • ๋˜ํ•œ ํ•ด๋‹น ๊ฒ€์ƒ‰์กฐ๊ฑด์ดย null์ผ ๊ฒฝ์šฐ ๋ฌด์‹œํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ๋™์  ์ฟผ๋ฆฌ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
  • where์ ˆ ์•ˆ์—๋Š” ์—ฌ๋Ÿฌ ๊ฒ€์ƒ‰ ์กฐ๊ฑด์ด ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.
    • eq(): A = ?
    • ne(): A != ?
    • eq().not(): A != ?
    • isNotNull(): A is not null
    • in(): A in (?)
    • notIn(): A not in (?)
    • between(): A between ?, ?
    • goe(): A >= ?
    • gt(): A > ?
    • loe(): A <= ?
    • lt(): A < ?
    • like(): A like ?
    • contains(): A like "%?%"
    • startWith(): A like "?%?

๊ฒฐ๊ณผ์กฐํšŒ

  • fetch(): ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•œ๋‹ค, ๊ฐ’์ด ์—†์„ ๋•Œ์—๋Š” ๋นˆ ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.
  • fetchOne(): ๋‹จ ๊ฑด ์กฐํšŒ
    • ๊ฒฐ๊ณผ๊ฐ€ ์—†์„ ๋•Œ์—๋Š”ย null์„ ๋ฆฌํ„ดํ•œ๋‹ค.
    • ๊ฒฐ๊ณผ๊ฐ€ ๋‘˜ ์ด์ƒ์ผ ๋•Œ์—๋Š”ย NonUniqueResultException์ด ๋ฐœ์ƒํ•œ๋‹ค.
  • fetchFirst():ย limit(1).fetchOne()๊ณผ ๋™์ผํ•˜๋‹ค.
  • fetchResults(): ํŽ˜์ด์ง• ์ •๋ณด๋ฅผ ํฌํ•จํ•˜์—ฌ total count์ฟผ๋ฆฌ๋ฅผ ์ถ”๊ฐ€๋กœ ์‹คํ–‰์‹œํ‚จ๋‹ค.
    • ์ดํ›„ย getTotal()๊ณผย getResult()๋ฉ”์„œ๋“œ๋กœ ์ •๋ณด๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค.
  • fetchCount(): count์ฟผ๋ฆฌ๋กœ ๋ณ€ํ˜•ํ•ด count์ˆ˜๋ฅผ ์กฐํšŒํ•œ๋‹ค.

ํŽ˜์ด์ง•

QueryResults<Member> queryResults = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)
                .limit(2)
                .fetchResults();
  • offset(): ์‹œ์ž‘์ ์„ ์„ค์ •ํ•œ๋‹ค.
  • limit(): ์ตœ๋Œ€ ์กฐํšŒ ์ˆ˜๋ฅผ ์„ค์ •ํ•œ๋‹ค.
  • listResults():ย QueryResultsํƒ€์ž…์œผ๋กœ ๋ฆฌํ„ด๋ฐ›๋Š”๋‹ค.
    • QueryResults.getTotal(): ์ „์ฒด ์ˆ˜ ์กฐํšŒ
    • QueryResults.getLimit(): limit ์กฐํšŒ
    • QueryResults.getOffset(): offset ์กฐํšŒ
    • QueryResults.getResults(): ๊ฒฐ๊ณผ๊ฐ’ ์กฐํšŒ

์ •๋ ฌ

  • ์ •๋ ฌ์€ย orderBy๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  • asc(): ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ
  • desc(): ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ
  • nullsLast(),ย nullsFirst: null๊ฐ’์˜ ์œ„์น˜๋ฅผ ์„ค์ •ํ•œ๋‹ค.
    • asc().nullsLast()์™€ ๊ฐ™์€ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

์ง‘ํ•ฉ

List<Tuple> result = queryFactory
                .select(team.name, member.age.avg())
                .from(member)
                .join(member.team, team)
                .groupBy(team.name)
                .fetch();
  • JPQL์ด ์ œ๊ณตํ•˜๋Š” ๋ชจ๋“  ์ง‘ํ•ฉ ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•œ๋‹ค.
  • ์ง‘ํ•ฉ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด tuple๋กœ ๊ฒฐ๊ณผ๊ฐ’์ด ๋ฆฌํ„ด๋œ๋‹ค.
  • ํ•จ์ˆ˜
    • COUNT(): ๊ฐœ์ˆ˜
    • SUM(): ํ•ฉ
    • AVG(): ํ‰๊ท 
    • MAX(): ์ตœ๋Œ€
    • MIN(): ์ตœ์†Œ
  • groupBy(): ํ•„๋“œ๋ช…์„ ์ด์šฉํ•ด ๊ทธ๋ฃนํ™”ํ•œ๋‹ค.
  • having(): having์ ˆ ์‚ฌ์šฉ

์กฐ์ธ

List<Member> result = queryFactory
                .selectFrom(member)
                .join(member.team, team)
                .where(team.name.eq("teamA"))
                .fetch();
  • join์€ย .join(์กฐ์ธ๋Œ€์ƒ, ๋ณ„์นญ์ฟผ๋ฆฌํƒ€์ž…)์˜ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.
    • innerJoin(): sql์˜ innerJoin
    • leftJoin(): sql์˜ leftJoin
    • rightJoin(): sql์˜ rightJoin
    • fullJoin(): sql์˜ fullJoin
  • on(): join์˜ on์ ˆ์„ ์‚ฌ์šฉํ•œ๋‹ค.
    • on์ ˆ์„ ์‚ฌ์šฉํ•ด ์กฐ์ธ ๋Œ€์ƒ์„ ํ•„ํ„ฐ๋งํ•˜๋Š” ๊ฒƒ์€ย innerJoin์‚ฌ์šฉ์‹œย where์ ˆ๋กœ ํ•„ํ„ฐ๋งํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•˜๋‹ค.
    • ๋‚ด๋ถ€์กฐ์ธ์ด๋ฉด where์„ ์‚ฌ์šฉํ•˜๊ณ , ์™ธ๋ถ€์กฐ์ธ์ธ ๊ฒฝ์šฐ์— ์‚ฌ์šฉํ•œ๋‹ค.
  • fetch(): join์— ํŽ˜์น˜์กฐ์ธ์„ ์ ์šฉํ•œ๋‹ค.
    • innerJoin(~~).fetch()์˜ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

์„œ๋ธŒ์ฟผ๋ฆฌ

QMember memberSub = new QMember("memberSub");

List<Member> result = queryFactory
        .selectFrom(member)
        .where(member.age.eq(
                JPAExpressions
                    .select(memberSub.age.max())
                    .from(memberSub)
        ))
        .fetch();
  • ์„œ๋ธŒ์ฟผ๋ฆฌ๋Š”ย JPAExpressions๋ฅผ ์ด์šฉํ•œ๋‹ค.
  • ์ฃผ๋กœ ์ƒˆ๋กœ์šดย QType์„ ์ •์˜ํ•˜์—ฌ ์‚ฌ์šฉํ•œ๋‹ค.
  • static import๋ฅผ ์ด์šฉํ•˜๋ฉด ๊น”๋”ํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
  • FROM์ ˆ์˜ ์„œ๋ธŒ์ฟผ๋ฆฌ
    • JPA JPQL์—์„œ๋Š” FROM์ ˆ์˜ ์„œ๋ธŒ์ฟผ๋ฆฌ๋ฅผ ํ—ˆ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
    • ๋”ฐ๋ผ์„œ ์„œ๋ธŒ์ฟผ๋ฆฌ๋ฅผ join๋ณ€๊ฒฝ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฟผ๋ฆฌ ๋ถ„๋ฆฌ, nativeSQL์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์•ˆ์œผ๋กœ ํ•ด๊ฒฐํ•ด์•ผ ํ•œ๋‹ค.

CASE๋ฌธ

List<String> result = queryFactory
            .select(member.age
                    .when(10).then("์—ด์‚ด")
                    .when(20).then("์Šค๋ฌด์‚ด")
                    .otherwise("๊ธฐํƒ€")
            )
            .from(member)
            .fetch();
  • when(): sql์˜ when
  • then(): when์— ํ•ด๋‹นํ•  ๋•Œ
  • otherwise(): ๋‚˜๋จธ์ง€ ์ƒํ™ฉ

์กฐ๊ฑด์„ ๋ณ€์ˆ˜๋กœ ์„ ์–ธํ•˜๊ธฐ

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, Expressions.constant("A"))
                .from(member)
                .fetch();

List<Tuple> result = queryFactory
                .select(member.username.concat("_").concat(member.age.stringValue()), member.age)
                .from(member)
                .where(member.username.eq("member1"))
                .fetch();
  • Expressions.constant(): ์ƒ์ˆ˜๋ฅผ ๋”ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.
  • concat(): ๋ฌธ์ž๋ฅผ ๋”ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

๋งˆ๋ฌด๋ฆฌ

Querydsl ์„ค์ •๊ณผ ๊ธฐ๋ณธ ๋ฌธ๋ฒ•์„ ๋‹ค๋ฃจ์—ˆ๋‹ค. ์›ฌ๋งŒํ•ด์„œ๋Š” Querydsl๋กœ ์ฟผ๋ฆฌ๋ฅผ ๋‹ค ์งค ์ˆ˜ ์žˆ๊ฒ ๋‹ค๊ณ  ํ•˜์ง€๋งŒ ๋งŒ์•ฝ ๊ทธ๋ ‡์ง€ ๋ชปํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒํ•  ๋• ๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ๋ฅผ ์ด์šฉํ•˜๋ฉด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค.

์ฒ˜์Œ ์‚ฌ์šฉํ•  ๋•Œ ์ž๋ฐ”๋กœ ์ฟผ๋ฆฌ๋ฅผ ์ง ๋‹ค๊ณ  ํ•˜์—ฌ ์–ด๋–ป๊ฒŒ ํ•˜๋Š” ๊ฑด์ง€ ๊ฐ์ด ์•ˆ ์™”์ง€๋งŒ ์‚ฌ์šฉํ•ด ๋ณด๋ฉด์„œ ์žฅ์ ์ด ๋งŽ์•„์„œ ๊ณ„์† ์‚ฌ์šฉํ•  ๊ฒƒ ๊ฐ™๋‹ค. Querydsl์— ๋Œ€ํ•œ ๋” ๋งŽ์€ ๋‚ด์šฉ๋“ค์ด ์žˆ์œผ๋‹ˆ ๋‹ค์Œ์— ๋” ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž.


์ถœ์ฒ˜: https://well-made-codestory.tistory.com/59?category=1098576 [SJ BackEnd Log:ํ‹ฐ์Šคํ† ๋ฆฌ]

์ถœ์ฒ˜: https://well-made-codestory.tistory.com/59?category=1098576 [SJ BackEnd Log:ํ‹ฐ์Šคํ† ๋ฆฌ]

์ถœ์ฒ˜: https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84/dashboard [์‹ค์ „! Querydsl - ์ธํ”„๋Ÿฐ (๊น€์˜ํ•œ)]

0๊ฐœ์˜ ๋Œ“๊ธ€