QueryDSL은 정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해주는 프레임워크입니다.
그래서 이거 왜 쓰나요?
Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용 하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL을 작성해야 합니다.
간단한 로직을 작성하는데 큰 문제는 없지만, 복잡한 로직의 경우 개행이 포함된 쿼리 문자열이 상당히 길어지게 되며 JPQL 문자열에 오타 혹은 문법적인 오류가 존재하는 경우, 정적 쿼리라면 애플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생합니다.
이러한 문제를 어느정도 해소하는데 기여하는 프레임워크가 QueryDSL이기 때문에 사용합니다.
QueryDSL의 장점
문자가 아닌 코드로 쿼리를 작성 함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
자동 완성 등 IDE의 도움을 받을 수 있다.
동적인 쿼리 작성이 편리하다.
쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용 할 수 있다.
QueryDSL의 원칙
QueryDSL의 핵심 원칙은 타입 안전성(Type Safety)입니다. 도메인 타입의 프로퍼티를 반영해서 생성한 쿼리 타입을 애용해 쿼리를 작성하게 됩니다. 또한 완전한 타입에 안전한 방법으로 함수/메서드 호출이 이루어집니다.
다른 원칙은 일관성(consistency)입니다. 기반 기술에 상관없이 쿼리 경로와 오퍼레이션은 모두 동일하며 Query 인터페이스는 공통의 상위 인터페이스를 갖게 됩니다.
2️⃣ QueryDSL 설정
QueryDSL 적용하면서 가장 까다로운 부분이 설정이라고 할 수 있다. 공식 문서에는 Gradle에 대한 내용이 누락되어 있으며, 실제로 QueryDSL 설정 방법은 Gradle 및 IntelliJ 버전에 따라 상이하기 때문이다.
build.gradle 파일 설정
buildscript
plugins 추가
dependencies
queryDSL 설정
querydsl 플러그인 task를 실행했을 때 Q클래스 파일 위치를 지정해주는 설정
yml 파일설정
datasource : database 설정값을 세팅한다.
jpa.database-platform : platform 설정
jpa.open-in-view : 영속성을 어느 범위까지 설정할지 결정
jpa.show-sql : 실행하는 쿼리 show 설정
jpa.hibernate.ddl-auto : 톰캣 기동할 때 어떤 동작을 할지 결정
해당 설정을 잘못하면 테이블이 drop 될 수 있다.
한번 설정이 끝났다면 none, validate로 설정하는 것을 추천한다.
jpa.properties.hibernate.format_sql : 쿼리를 잘 정렬해서 보여준다.
APT를 이용하여 Entity 기반으로 querydsl plugin을 실행시키면 prefix “Q”가 붙는 Q클래스가 생성된다.
User → QUser
APT란? Annotation Processing Tool의 약자로 Annotation이 있는 코드 기준으로 새로운 파일을 만들 수 있고 compile 기능도 가능하다.
도메인이 바뀐다면 위의 작업을 다시 해주어야 합니다.
3️⃣ QueryDSL 사용해보기
예제 도메인
JpaQueryFactory
static import를 사용하면 더욱 가독성이 좋아집니다.
다건 유저 조회
🔥
유저 정보가 저장소에 없으면 빈 리스트를 반환하게 됩니다.
방법 1 - fetch()
방법 2- fetchResults()
단건 유저 조회
🔥
단건 조회로 결과가 하나라면 해당 User 객체 반환 결과가 없으면 null 결과가 둘 이상이라면 NonUniqueResultException 에외를 반환하게 됩니다.
방법 1 - fetchOne()
방법 2 - fetchFirst();
조건에 맞는 유저 조회
eq() : equlas, == 연산자로 생각하시면 됩니다.
and의 경우에 and 키워드 말고 ,(콤마)로 구분이 가능합니다.
ne() : != 연산자로 생각하시면 됩니다.
eq().not() 방식으로 사용할 수도 있어요 !
isNotNull() : 필드값이 null이 아닌 것
in() : 안의 요소가 포함되는 값 조회
notIn() : 요소에 포함되지 않는 값 조회
between() : 요소 사이에 포함되는 값
goe() : 값이 요소보다 ≥ 인것 조회
gt() : 값이 요소보다 > 인것 조회
loe() : 값이 요소보다 ≤ 인것 조회
lt() : 값이 요소보다 < 인것 조회
like() : like 문법과 동일합니다
정렬기능
asc() : 오름차순 정렬
desc() : 내림차순 정렬
nullLast(), nullFirst() : null 데이터 순서 부여
페이징 기능
방법 1 - fetch()
방법 2 - fetchResults()
추천하는 방법도 아니며 현재 deprecated 된 상태이다.
집계 기능
count() : 수
sum() : 합계
avg() : 평균
max() : 최대
min() : 최소
조인 기능
join(), innerJoin() : 내부 조인(inner join)
leftJoin() : left 외부 조인
rightJoin() : right 외부 조인
서브쿼리
서브 쿼리는 JPAExpressions 를 통해서 표현해야 한다. 서브쿼리에 사용되는 QClass는 겉의 쿼리와 다른 객체여야 하므로 QClass를 직접 선언해서 다른 인스턴스를 사용하도록 해야한다.
JPA JPQL 서브쿼리의 한계점으로는 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.
당연히 QueryDSL도 지원하지 않으며 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. QueryDSL도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.
from 절의 서브쿼리 해결법
서브쿼리를 join으로 변경한다. (가능한 상황도 있으며 불가능한 상황도 존재한다.)
애플리케이션에서 쿼리를 2번 분리해서 실행한다.
nativeSQL을 사용한다.
case문
case문은 select, 조건절(where), order by에서 사용 가능하다.
상수, 문자 더하기
문자가 아닌 타입들은 stringValue()를 통해서 문자로 변환할 수 있으며 이 방법은 ENUM을 처리할 때도 자주 쓰인다.
4️⃣ QueryDSL 추가사항
DTO 반환하기
DTO 생성을 위한 new 키워드가 필요하며 패지키 풀 경로까지 작성해줘야 합니다.
생성자 방식만 지원합니다.
QeuryDSL Bean 생성
db를 다루다 보면 종종 projection을 다뤄야 할 때가 있다.
Projection 연산
한 Relation의 Attribute들의 부분 집합을 구하는 연산자로 결과로 생성되는 Relation은 스키마에 명시된 Attribute들만 가진다.
결과로 나온 Relation은 기본 키가 아닌 Attribute 에 대해서만 중복된 tuple들이 존재할수 있다.
프로퍼티 접근 - Setter
필드 직접 접근
필드가 다르다면 alias를 이용하자 !
생성자 사용
@QueryProjection + 생성자
컴파일러로 타입 체크가 가능합니다 ! 가장 안전한 방법!
QueryDSL 애노테이션이 DTO에 침식해 있다는 점과 QClass를 생성해 주어야 함
동적쿼리
BooleanBuilder
where 다중 파라미터 사용하기
where 조건에 null 값은 무시된다 !
메서드를 다른 쿼리에서도 재활용이 가능하다 !
쿼리 자체의 가독성도 좋아진다 !
🔥
조합하는 경우에는 null을 조심해야 한다 !
수정, 삭제 벌크
계산의 경우, add() 등의 메서드를 활용하면 된다.
🔥
JPQL 배치와 같이 영속성 컨텍스트를 무시하고 실행한다.실행 후 영속성 컨텍스트를 초기화하는 것이 안전하다.
@Query("select p from Post p join fetch p.user u "
+ "where u in "
+ "(select t from Follow f inner join f.target t on f.source = :user) "
+ "or u = :user "
+ "order by p .createdAt desc")
List<Post> findAllAssociatedPostsByUser(@Param("user") User user, Pageable pageable);
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
@Configuration
public class DataBaseConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
protected User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
// GETTER
}
@Entity
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
protected Team() {}
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public Team(String name) {
this.name = name;
}
// GETTER
}
List<User> user = jpaQueryFactory.selectFrom(user).fetch();
// fetchResults() 페이징 정보 포함, total count 쿼리 추가
QueryResults<User> userQueryResults = jpaQueryFactory.selectFrom(user)
.fetchResults();
List<User> users = userQueryResults.getResults();
// null일 경우에 첫번째로 정렬할 것인지 마지막으로 정렬할 것인지 !
// nullFirst() 라면 정렬 기준이 asc, desc 상관없이 첫번째로 온다!
List<User> users = jpaQueryFactory.selectFrom(user)
.where(user.age.eq(27))
.orderBy(user.name.asc().nullsFirst())
.fetch();
// 유저 수, 유저 나이의 합, 평균 나이, 최대 나이, 최소 나이를 구해줘 !
List<Tuple> tuples = jpaQueryFactory
.select(
user.count(),
user.age.sum(),
user.age.avg(),
user.age.max(),
user.age.min()
)
.from(user)
.fetch();
// 그룹화된 결과를 제한하려면 having 절과 함께 사용하면 된다.
List<Tuple> result = jpaQueryFactory
.select(team.name, user.age.avg())
.from(user)
.join(user.team, team)
.groupBy(team.name)
.fetch();
List<Member> result = jpaQueryFactory
.selectFrom(user)
.join(user.team, team)
.where(team.name.eq("규현팀"))
.fetch();
// fetchJoin()을 해주면 연관관계 매핑된 정보도 함께 가져온다.
List<Member> result = jpaQueryFactory
.selectFrom(user)
.join(user.team, team).fetchJoin()
.where(team.name.eq("규현팀"))
.fetch();
// 유저의 나이가 현재 데이터베이스에 있는 유저 평균 나이 이상인 사람만 조회해줘 !
QUser userSub = new QUser("userSub");
List<User> result = jpaQueryFactory
.selectFrom(user)
.where(user.age.goe(
JPAExpressions
.select(userSub.age.avg())
.from(userSub)))
.fetch();
entityManager.createQuery(
"select new 패지키 풀 경로.클래스이름(파라미터) FROM 테이블", 반환클래스)
).getResultList();
// 에시
entityManager.createQuery(
"SELECT new com.prgrms.querydsl.example.UserDto(u.name, u.age) FROM User u", UserDto.class
).getResultList();