일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- c#
- 강의실2
- 유니티
- 마우스 따라다니기
- 18249
- SWEA
- 윈도우
- 단어 수학
- 영상 프레임 추출
- 탄막 이동
- 탄막
- 2020 KAKAO BLIND RECRUITMENT
- 백준
- 수 만들기
- mysqld.sock
- AI Hub
- 그리디알고리즘
- 알고리즘
- 원형
- 회의실 배정
- 3273
- 탄막 스킬 범위
- 3344
- 걷는건귀찮아
- 자료구조 목차
- 우분투
- 알고리즘 목차
- 문자열 압축
- MySQL
- 토글 그룹
- Today
- Total
와이유스토리
[도트타이머] 7. QueryDSL 탐험기 - JPA 객체 지향 쿼리(정적 쿼리, 동적 쿼리, QueryDSL 설치 및 사용) 본문
[도트타이머] 7. QueryDSL 탐험기 - JPA 객체 지향 쿼리(정적 쿼리, 동적 쿼리, QueryDSL 설치 및 사용)
유(YOO) 2022. 12. 23. 22:37Repository에서 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 방법
- Projections.bean
- Projections.filter
- Projections.constructor
- @QueryProjection
Fetch 방법
- fetch
- fetchFirst
SubQuery 방법
- Select
- 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://jddng.tistory.com/m/335
http://honeymon.io/tech/2020/07/09/gradle-annotation-processor-with-querydsl.html
'프로젝트 > 백엔드' 카테고리의 다른 글
[도트타이머] 6. 예외처리(CustomException, ExceptionHandler, JwtAuthenticationEntryPoint, JwtAccessDeniedHandler) (0) | 2022.12.22 |
---|---|
[도트타이머] 4. JPA 폴더링 및 간단한 회원가입 구현(보안X) (0) | 2022.12.14 |
[도트타이머] 3. API 명세서 작성 (0) | 2022.12.13 |
[도트타이머] 2. 스프링부트에 MySQL 연동 (0) | 2022.12.12 |
[도트타이머] 1. ERD모델 만들기(aka. 처음 설계할 때 잘하자) (0) | 2022.12.11 |