와이유스토리

[도트타이머] 7. QueryDSL 탐험기 - JPA 객체 지향 쿼리(정적 쿼리, 동적 쿼리, QueryDSL 설치 및 사용) 본문

프로젝트/백엔드

[도트타이머] 7. QueryDSL 탐험기 - JPA 객체 지향 쿼리(정적 쿼리, 동적 쿼리, QueryDSL 설치 및 사용)

유(YOO) 2022. 12. 23. 22:37

Repository에서 DB로 쿼리를 요청할 때 사용한다. SQL Mapper에는 Mybatis 등을 이용하며, ORM에서는 아래 방식으로 쿼리를 요청할 수 있다. SQL Query문의 변수 유무에 따라 정적 쿼리와 동적 쿼리로 나눌 수 있다.

 

정적 쿼리

1. @Named Query

  • Entity에 쿼리 지정
  • 실무에서 사용X

2. Query Method

  • Spring Data JPA에서 JPQL 자동 생성
  • findbyId나 findAll과 같이 직접 정의하지 않아도 사용할 수 있는 쿼리

3. @Query

  • Repository 메소드에 SQL Query문 직접 작성하여 @Named Query 호출

동적 쿼리

1. 순수 JPQL

2. Criteria & Specifiation

  • 실무에서 사용X

3. QueryDSL

  • 가독성, 사용성 등 좋음

QueryDSL 적용

1. applicaion.properties

applicaion.properties에 QueryDSL 의존성과 QueryDSL 클래스 경로를 추가한다. 설정이 까다로워서 고생했는데 아래 참고 블로그들에서 자세히 설명해주셨다.

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

group = 'com' // 회사 도메인 거꾸로
version = '0.0.1-SNAPSHOT'  // SNAPSHOT 개발용, RELEASE 배포용
sourceCompatibility = '17' // JAVA version

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
	runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'

	// QueryDSL
	implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
	annotationProcessor("jakarta.persistence:jakarta.persistence-api") // java.lang.NoClassDefFoundError 처리(javax.annotation.Entity)
	annotationProcessor("jakarta.annotation:jakarta.annotation-api")
}

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

// QueryDSL Q클래스 경로
sourceSets {
	main {
		java {
			srcDirs = ["$projectDir/src/main/java", "$projectDir/build/generated"]
		}
	}
}

com.ewerk.gradle.plugins.querydsl은 버전이 오래되어서 이것저것 추가할 것이 많다고 한다.(아래 블로그 참고)

 

View 탭 > Tool Windows > gradle

gradle 탭 >Tasks > other > compile.JAVA 실행

build에 generated 생성 되고 엔티티별로 Q클래스 자동 생성

한 가지 단점은 새로 Run이나 Debug할 때 Q클래스가 이미 존재한다는 에러가 나면 generated 폴더를 삭제해야 한다.

2. QueryDSLConfig.class

package com.dotetimer.config;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.querydsl.jpa.impl.JPAQueryFactory;

@Configuration
public class QueryDSLConfig {
    @PersistenceContext
    private EntityManager entityManager;

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

3. QueryDSL Interface 작성

package com.dotetimer.repository;

import com.dotetimer.domain.*;

import java.util.List;

// QueryDSL
public interface QueryDSLRepository {
    // User
    List<User> findUsersByName(String userName);

    // Group
    List<StudyGroup> findGroupsByKeyword(String keyword);
    // GroupJoin findByUserAndGroup(int userId, int groupId); // 동적 쿼리
    List<GroupJoin> findUsersByGroup(int groupId);
    // List<StudyGroup> findGroupsByUser(int userId);

    // Review
    // Review findByReviewedAt(int userId, LocalDate localDate);
    // ReviewLike findByUserAndReview(int userId, int reviewId); // 동적 쿼리
    List<Review> findReviewsExceptUser(int userId);
    List<ReviewLike> findLikesByReview(int reviewId);

    // Plan
    // List<Plan> findPlansByUserAndDate(boolean record, int userId, LocalDate studiedAt); // 동적 쿼리 : record 필수
}

4. QueryDSL Interface 구현체 작성

이미 존재하는 쿼리들까지 작성해버려서 시간 낭비했다... 기계적으로 작성하지 말고 확인하고 생각하면서 작성해야 몸이 덜 고생한다는 걸 잊지 말아야겠다.

BooleanExpression

BooleanBuilder를 이용해 여러개 속성을 확인할 수 있는 쿼리 작성이 가능하다

Projection 방법

  1. Projections.bean
  2. Projections.filter
  3. Projections.constructor
  4. @QueryProjection

Fetch 방법

  1. fetch
  2. fetchFirst

SubQuery 방법

  1. Select
  2. Where
package com.dotetimer.repository;

import com.dotetimer.domain.*;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;

import java.time.LocalDate;
import java.util.List;

import static com.dotetimer.domain.QUser.*;
import static com.dotetimer.domain.QGroupJoin.*;
import static com.dotetimer.domain.QStudyGroup.*;
import static com.dotetimer.domain.QReview.*;
import static com.dotetimer.domain.QReviewLike.*;
import static com.dotetimer.domain.QPlan.*;

@RequiredArgsConstructor
public class QueryDSLRepositoryImpl implements QueryDSLRepository {
    private final JPAQueryFactory jpaQueryFactory;
    
    // 사용자 이름으로 검색
    @Override
    public List<User> findUsersByName(String userName) {
        return jpaQueryFactory
                .select(user)
                .from(user)
                .where(user.name.contains(userName))
                .fetch();
    }

    @Override
    public List<StudyGroup> findGroupsByKeyword(String keyword) {
        return jpaQueryFactory
                .select(studyGroup)
                .from(studyGroup)
                .where(searchGroup(keyword))
                .fetch();
    }
    
    // 그룹 키워드나 카테고리로 검색

//    // 사용자나 그룹으로 참가 정보 찾기
//    @Override
//    public GroupJoin findByUserAndGroup(int userId, int groupId) {
//        return jpaQueryFactory
//                .select(groupJoin)
//                .from(groupJoin)
//                .where(eqUserId(userId, 1), eqGroupId(groupId))
//                .fetchFirst();
//    }

    // groupId에 속한 사용자 리스트
    @Override
    public List<GroupJoin> findUsersByGroup(int groupId) {
        return jpaQueryFactory
                .selectFrom(groupJoin)
                .where(groupJoin.user.in(
                        JPAExpressions
                                .select(groupJoin.studyGroup.user)
                                .from(groupJoin)
                                .where(groupJoin.studyGroup.id.eq(groupId))))
                .fetch();
    }

//    // userId가 속한 그룹 리스트
//    @Override
//    public List<StudyGroup> findGroupsByUser(int userId) {
//        return jpaQueryFactory
//                .selectFrom(studyGroup)
//                .where(studyGroup.id.in(
//                        JPAExpressions
//                                .select(groupJoin.studyGroup.id)
//                                .from(groupJoin)
//                                .where(groupJoin.user.id.eq(userId))))
//                .fetch();
//    }

//    // 작성날짜로 하루세줄 찾기
//    @Override
//    public Review findByReviewedAt(int userId, LocalDate localDate) {
//        return jpaQueryFactory
//                .selectFrom(review)
//                .where(review.user.id.eq(userId), review.reviewedAt.eq(localDate))
//                .fetchFirst();
//    }

//    // 사용자나 하루세줄로 하루세줄 좋아요 찾기
//    @Override
//    public ReviewLike findByUserAndReview(int userId, int reviewId) {
//        return jpaQueryFactory
//                .select(reviewLike)
//                .from(reviewLike)
//                .where(eqUserId(userId, 2), eqReviewId(reviewId))
//                .fetchFirst();
//    }

    // 본인 하루세줄 제외하고 찾기
    @Override
    public List<Review> findReviewsExceptUser(int userId) {
        return jpaQueryFactory
                .selectFrom(review)
                .where(review.user.id.ne(userId)) // not equal
                .fetch();
    }

    @Override
    public List<ReviewLike> findLikesByReview(int reviewId) {
        return jpaQueryFactory
                .selectFrom(reviewLike)
                .where(reviewLike.review.id.eq(reviewId))
                .fetch();
    }

//    @Override
//    public List<Plan> findPlansByUserAndDate(boolean record, int userId, LocalDate studiedAt) {
//        return jpaQueryFactory
//                .select(plan)
//                .from(plan)
//                .where(plan.recorded.eq(record), eqUserId(userId, 3), eqPlanDate(studiedAt))
//                .fetch();
//    }

    private BooleanExpression eqUserId(int userId, int order) {
        if (!StringUtils.hasText(String.valueOf(userId))) return null;
        if (order == 1) return groupJoin.user.id.eq(userId); // groupJoin
        if (order == 2) return reviewLike.user.id.eq(userId); // reviewLike
        //if (order == 3) return plan.user.id.eq(userId); // plan
        return null;
    }

    private BooleanExpression eqGroupId(int groupId) {
        if (!StringUtils.hasText(String.valueOf(groupId))) return null;
        return groupJoin.studyGroup.id.eq(groupId);
    }

    private BooleanExpression eqReviewId(int reviewId) {
        if (!StringUtils.hasText(String.valueOf(reviewId))) return null;
        return reviewLike.review.id.eq(reviewId);
    }

//    private BooleanExpression eqPlanDate(LocalDate studiedAt) {
//        if (!StringUtils.hasText(String.valueOf(studiedAt))) return null;
//        return plan.studiedAt.eq(studiedAt);
//    }

    private BooleanBuilder searchGroup(String keyword) {
        BooleanBuilder booleanBuilder = new BooleanBuilder();
        if (!StringUtils.hasText(keyword)) return null;
        booleanBuilder.or(studyGroup.name.contains(keyword));
        booleanBuilder.or(studyGroup.details.contains(keyword));
        booleanBuilder.or(studyGroup.category.contains(keyword));
        return booleanBuilder;
    }
}

5. QueryDSL Interface 상속

package com.dotetimer.repository;

import com.dotetimer.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

// Spring Data JPA에서 JPQL 생성
public interface UserRepository extends JpaRepository<User, Integer>, QueryDSLRepository {
    Optional<User> findByEmailAndName(String email, String name);
}

 

* 참고

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.details

https://jddng.tistory.com/m/335

http://honeymon.io/tech/2020/07/09/gradle-annotation-processor-with-querydsl.html

https://vesselsdiary.tistory.com/146

Comments