코딩마을방범대
SpringBoot의 QueryDSL 본문
QueryDSL 을 사용해야 하는 이유
QueryMethod
- 주어진 명령어를 통해 한정적인 쿼리만 생성 가능
- 컴파일 시 오류를 발견할 수 있음
JPQL
- 사용자가 원하는 쿼리를 자유롭게 생성할 수 있음
- 문법 오류가 있는 경우에도 컴파일 시 오류를 잡을 수 없고 런타임 때 확인 가능
- 개행이 포함되어 복잡한 쿼리의 경우 문법이 복잡해짐
세팅하기
build.gradle 설정
java 1.8
spring boot 2.7.11
스프링부트 버전 별 설정법
dependencies {
...
//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"
}
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
annotationProcessor
Java 컴파일러 플러그인
컴파일 단계에서 어노테이션을 분석 및 처리함으로써 추가적인 파일을 생성할 수 있음
- querydsl-apt가 @Entity 및 @Id 등의 어노테이션을 알 수 있도록, javax.persistence과 javax.annotation을 annotationProcessor에 함께 추가
- 프로젝트 내의 @Entity 어노테이션을 선언한 클래스를 탐색하고, JPAAnnotationProcessor를 사용해 Q 클래스를 생성
Configuration 설정
JPAQueryFactory를 Bean으로 등록하여 프로젝트 전역에서 QueryDSL을 작성할 수 있도록 함
@PersistenceContext
빈으로 등록되어 있는 EntityManagerFactory를 통해 EntityManager를 주입 받음
@Configuration
public class QueryDSLConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
사용 방법
1️⃣ Repository에서 사용하기
1. 명령어를 정의 해놓을 클래스 생성
- 명령어에 대해 서술해놓지 않고 정의만 해놓는 클래스
public interface UserCustomRepository {
void signUpUser(UserRequest request);
Optional<User> loginUser(String userKey, String pw);
Page<UserResponse> read(String kewWord, Pageable pageable);
}
2. 명령어에 대해 서술해놓을 클래스 생성
- 2번에 생성해놓은 명령어에 대해 서술해놓는 클래스
- Q클래스를 static으로 import 함으로써, Q클래스에 미리 정의된 Q 타입 인스턴스 상수를 사용
import static com.task.model.QUser.user;
@Repository
@RequiredArgsConstructor
public class UserCustomRepositoryImpl implements UserCustomRepository {
private final JPAQueryFactory jpaQueryFactory;
@Override
public void signUpUser(UserRequest request) {
jpaQueryFactory.insert(user)
.columns(
user.userKey, user.email, user.pw,
user.name, user.hp, user.agency
).values(
request.getUserKey(), request.getEmail(), request.getPw(),
request.getName(), request.getHp(), request.getAgency()
)
// .set(user.userKey, request.getUserKey())
// .set(user.email, request.getEmail())
// .set(user.pw, request.getPw())
// .set(user.name, request.getName())
// .set(user.hp, request.getHp())
// .set(user.agency, request.getAgency())
.execute();
}
@Override
public Optional<User> loginUser(String userKey, String pw) {
return Optional.ofNullable(jpaQueryFactory
.selectFrom(user)
.where(
user.userKey.eq(userKey),
user.pw.eq(pw)
)
.fetchOne());
}
@Override
public Page<UserResponse> read(String kewWord, Pageable pageable) {
List<UserResponse> users = jpaQueryFactory
.select(
new QUserResponse(
user.userKey, user.email, user.name, user.hp
)
)
.from(user)
.where(
user.userKey.contains(kewWord)
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(user.createDate.desc())
.fetch();
return new PageImpl(
users,pageable,userTotal(kewWord)
);
}
private Long userTotal(String keyWord){
return jpaQueryFactory.select(user.count())
.from(user)
.where(
userKeyContains(keyWord)
)
.fetchOne();
}
private BooleanExpression userKeyContains(String keyWord) {
return hasText(keyWord) ? user.userKey.contains(keyWord) : null;
}
}
3. 기존 Repository에 상속시키기
@Repository
public interface UserRepository extends JpaRepository<User, String>, UserCustomRepository {
...
}
2️⃣ Q클래스를 인스턴스로 생성하여 필요할 때마다 정의하여 사용하기
@RequiredArgsConstructor
class Task12QueryDSLApplicationTests {
JPAQueryFactory queryFactory;
@DisplayName("QueryDSL 테스트 메소드 - userKey에 gu를 포함하며 agency가 SKT인 유저를 내림차순 정렬")
public void test(){
QUser qUser = QUser.user;
List<User> users = queryFactory
.selectFrom(qUser)
.where(
qUser.userKey.contains("gu"),
qUser.agency.eq("SKT")
)
.orderBy(qUser.createDate.desc())
.fetch();
}
}
QueryDSL의 다양한 사용법
Join 처리하는 방법
QUser user = QUser.user;
QMsg msg = QMsg.msg;
List<User> list =
query.selectFrom(user)
.join(user.msg, msg)
.where(user.name.eq("gu"))
.fetch();
페이징 처리하는 방법
List<User> list =
queryFactory.selectFrom(user)
.where(user.age.gt(18))
.orderBy(user.name.desc())
// 페이징을 위해 limit, offset도 그냥 넣어줄 수 있다.
.limit(10)
.offset(10)
.fetch();
QueryDSL에서의 코드 재활용
중복되는 부분을 함수로 정의하여 재활용할 수 있음
return queryFactory.selectFrom(coupon)
.where(
coupon.type.eq(typeParam),
isServiceable()
)
.fetch();
}
...
private BooleanExpression isServiceable() {
return coupon.status.wq("LIVE")
.and(marketing.viewCount.lt(markting.maxCount));
}
boolean 값을 이용해 쿼리 실행 여부 결정하기
1.BooleanBuilder
어떤 쿼리가 나가는지 예측하기 힘들다는 단점이 있음
BooleanBuilder builder = new BooleanBuilder();
if(name != null) {
builder.and(user.name.contains("gu"));
}
if(age != 0) {
builder.and(user.age.gt(9));
}
List<User> list = queryFactory.selectFrom(user)
.where(builder)
.fetch();
2. BooleanExpression
null 을 반환하게 되면 Where 절에서 조건이 무시되기 때문에 안전함
private BooleanExpression userNameEq(String userName) {
return hasText(userName) ? user.name.eq(userName) : null;
}
...
List<User> list =
queryFactory.selectFrom(user)
.where(userNameEq(request.userName))
.fetch();
Entity가 아닌 DTO 값으로 받기
반환 받을 DTO를 Q클래스로 생성해주기 위해선
생성자에 @QueryProjection를 붙여주면 됨
@QueryProjection
public UserResponse(String userKey, String email, String name, String hp) {
this.userKey = userKey;
this.email = email;
this.name = name;
this.hp = hp;
}
Query 구현 시 Q클래스로 select
@Override
public Optional<UserResponse> loginUser(String userKey, String pw) {
return Optional.ofNullable(jpaQueryFactory
.select(
new QUserResponse(
user.userKey, user.email, user.name, user.hp
)
)
.from(user)
.where(
user.userKey.eq(userKey),
user.pw.eq(pw)
)
.fetchOne());
}
💡 TIPS!
1. extends / implements 사용하지 않기
- 매번 상속받아 Repository를 구현하는 것이 불편하기도 하고
JpaQueryFactory 만 있다면 구현에는 영향이 없기 때문에
상속 받는 구조보단 기존 Repository에 JpaQueryFactory 만 bean으로 주입받는 것이 편리
@Repository
@RequiredArgsConstructor
public class UserRepositoryCustom {
private final JpaQueryFactory queryFactory;
// query문 선언
}
2. 동적쿼리는 BooleanExpression 사용하기
- BooleanBuilder 는 어떤 쿼리가 나가는지 예측하기 힘들다는 단점이 있음
- BooleanExpression 은 null 을 반환하게 되면 Where 절에서 조건이 무시되기 때문에 안전
// BooleanBuilder
BooleanBuilder builder = new BooleanBuilder();
if (hasText(condition.getUsername())) {
builder.and(member.username.eq(condition.getUsername()));
}
// BooleanExpression
private BooleanExpression userNameEq(String userName) {
return hasText(userName) ? user.name.eq(userName) : null;
}
3. exist 메소드 사용하지 않기
- Querydsl 에 있는 exist 는 count 쿼리를 사용하므로
전체 행을 모두 조회해서 성능이 떨어짐
(SQL exist 쿼리는 첫번째로 조건에 맞는 값을 찾는다면 바로 반환) - exist 대신 fetchFirst()를 사용해 결과를 한 개만 가져오고 끝내도록 하기
4. 조회할땐 Entity 보다는 Dto 를 우선적으로 가져오기
- Entity로 가져올 경우 불필요한 칼럼을 조회하기도 하며,
연관관계 매핑이 된 경우 N + 1 문제가 생길 수 있음
5. Select 칼럼에 Entity는 자제하기
- select 안에 Entity를 넣어 조회하면,
Entity에 있는 모든 컬럼을 조회하기 때문에 효율성이 떨어짐
❗❗ insert 시 org.hibernate.hql.internal.ast.QuerySyntaxException: unexpected token 오류가 발생하는 경우 ❗❗
JPAQueryFactory 의 경우 Hibernate 6.0 버전 이상부터 insert를 지원함
5.6 버전을 이용할 경우 EntityManager의 persist를 이용해 insert 하는 방법이 있음
아래 사이트를 확인해보면 5.6 버전에서는 Column 선택이 불가하다고 함
(삽입할 명시적 값을 지정할 수 없음)
docs 사이트
5.6 (supports INSERT-SELECT only)
6.0 (supports both INSERT-SELECT and INSERT-VALUES)
java 8 버전은 5.6 버전까지만 사용 가능함
EntityManager 선언 방법
사용할 클래스에 @RequiredArgsConstructor, @Transactional 선언 후 호출
@Repository
@RequiredArgsConstructor
@Transactional
public class UserRepository{
private final JPAQueryFactory jpaQueryFactory;
private final EntityManager entityManager;
...
public void signUpUser(UserRequest request) {
entityManager.persist(
User.of(request)
);
}
참고사이트
Spring Boot에 QueryDSL을 사용해보자 - Tecoble
[JPA] Spring Data JPA와 QueryDSL 이해, 실무 경험 공유 - Namjun Kim
우아한 형제들의 Querydsl 사용법 - youngerjesus.log
'💡 백엔드 > Java' 카테고리의 다른 글
SpringBoot Custom Annotation 생성하기 - (1) 파라미터에 부여 (0) | 2023.05.28 |
---|---|
JPA가 대문자 테이블명을 인식하지 못할 때 (0) | 2023.05.28 |
SpringBoot의 @Converter (0) | 2023.05.28 |
JWT - (2) Java에서 JWT 사용하기 (0) | 2023.05.28 |
비대칭키를 이용한 인증서 생성 - (3) 실무 코드 정리 (0) | 2023.05.27 |