SpringBoot의 QueryDSL

단비·2023년 5월 16일
0

학습

목록 보기
48/66

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);
    }
}





사용 방법


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에 직접 정의하지 않아도 repository에서 가져다 쓸 수 있음
@Repository
public interface UserRepository extends JpaRepository<User, String>, UserCustomRepository {
	...
}




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();
  1. 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

profile
tistory로 이전! https://sweet-rain-kim.tistory.com/

0개의 댓글