-
❓QueryDSL
💡 QueryDSL은 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 해주는 프레임워크
사용하는 이유?
- 실제로 Query를 작성하다보면 수 많은 쿼리를 수작업으로 생성해야 한다.
- 또한, 사람이 작성하면 Query는 컴파일 단계에서 오류가 있는지 없는지 알 수 없다.
JPQL
JPQL ( Java Persistence Query Language ) 는 엔티티 객체를 조회하는 객체지향 쿼리이다.
- 테이블이 아닌 객체를 검색하는 객체지향 쿼리
- SQL을 추상화 하여 특정 데이터베이스에 의존하지 않는다.
- 데이터베이스의 방언이 바뀌어도 수정하지 않아도 된다.
- JPA는 JPQL을 분석하여 SQL을 생성한 후 DB에서 조회한다.
특징
- SQL을 추상화한 JPA의 객체지향 쿼리
- Table이 아닌 Entity 객체를 대상으로 개발
- Entity와 속성은 대소문자 구분
- 별칭(alias) 사용이 필수
JPQL의 한계
- JPQL에서 복잡한 쿼리를 작성하다보면 오타가 발생할 수 있고, 가독성이 떨어질 수 있다.
- 문자열(String) 형태이기 때문에 개발자 의존적
- Compile 단계에서 Type-Check 가 불가능하다.
- Runtime 단계에서 오류를 발견할 수 있다.
Criteria 쿼리
Criteria는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API이다.
⇒ 문자가 아닌 프로그래밍 코드로 JPQL을 작성할 수 있다
장점
- 문법 오류를 컴파일 단계에서 확인이 가능하다.
- 문자 기반의 JPQL보다 동적 쿼리를 안전하게 생성할 수 있다.
단점
- 코드가 복잡해 직관적이지 못해 이해하기 어렵다.
// JPQL : select m from Member m // Criteria 쿼리 빌더 CriteriaBuilder cb = em.getCriteriaBuilder(); // Criteria 생성 + 반환 타입 지정 CriteriaQuery<Member> cq = cb.createQuery(Member.class); // FROM 절 Root<Member> m = cq.from(Member.class); // SELECT 절 cq.select(m); TypedQuery<Member> query = em.createQuery(cq); List<Member> members = query.getResultList();
- Criteria 쿼리는 빌더(CriteriaBuilder)가 필요하다.
- EntityManager 또는 EntityManagerFactory로 생성한다.
- Criteria 쿼리 빌더에서 Criteria 쿼리 (CriteriaQuery)를 생성한다.
- FROM 절을 생성하고,
- 반환된 값은 Criteria에서 사용하는 별칭인, 조회의 시작점이라는 의미로 쿼리 루트(Root)로 사용한다.
- SELECT 절 생성
// 검색 조건 추가 시 //JPQL /* select m from Member m where m.username = '회원1' order by m.age desc */ CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Member> cq = cb.createQuery(Member.class); Root<Member> m = cq.from(Member.class); // 검색 조건 정의 Predicate usernameEqual = cb.equal(m.get("username"), "회원1"); // 정렬 조건 정의 javax.persistence.criteria.Order ageDesc = cb.desc(m.get("age")); // 쿼리 생성 cq.select(m) .where(usernameEqual) //WHERE 절 생성 .orderby(ageDesc); //ORDER BY 절 생성 List<Member> resultList = em.createQuery(cq).getResultList();
⇒ 굉장히 복잡하다.
최근에는 Criteria를 거의 사용하지 않는다.
❗ QueryDSL
Criteria의 복잡하고 어렵다는 단점을 개선하기 위해 나온 프레임워크
QueryDSL은 JPQL의 빌더 역할 을 하고, JPA Criteria를 대체 할 수 있다.
특징
- 문자가 아닌 코드로 쿼리를 작성 ⇒ 컴파일 시점에 문법 오류를 쉽게 발견할 수 있다.
- 자동완성, IDE 의 도움을 받을 수 있다.
- 동적인 쿼리 작성이 편리
- 쿼리 작성 시 제약 조건 등을 메서드화하여 재사용할 수 있다.
- BooleanBuilder
- 도메인 타입과 프로퍼티를 안전하게 참조할 수 있다.
- 도메인 타입의 리팩터링을 더 잘 할 수 있다.
Gradle 설정
Gradle을 사용한 프로젝트의 QueryDSL 환경 설정에서 중요한 것
- Spring boot 버전
- Gradle 버전
- QueryDSL 버전
- 기존에 작업중인 Spring boot 프로젝트의 버전을 확인하여 버전에 맞는 설정을 하는 것이 중요하다.
springframework.boot - 2.7.2
gradle - 7.5
java - 11build.gradle
// 1. queryDsl version 정보 추가 buildscript { ext { queryDslVersion = "5.0.0" } } plugins { id 'org.springframework.boot' version '2.7.2' id 'io.spring.dependency-management' version '1.0.12.RELEASE' // 2. querydsl plugins 추가 id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" id 'java' } group = 'com.prgrms' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'mysql:mysql-connector-java' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' // 3. querydsl dependencies 추가 // 3-1. querydsl 추가 - QuerydslRepository, QueryDslPredicateExecutor 사용 가능 implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" // 3-2. querydsl-apt : 쿼리 타입(Q)를 생성할 때 필요한 라이브러리 annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}" } tasks.named('test') { useJUnitPlatform() } // 4. queryDSL 설정 추가 // 4-1. querydsl에서 사용할 경로 설정 def querydslDir = "$buildDir/generated/querydsl" // 4-2. JPA 사용여부와 사용할 경로 설정 querydsl { jpa = true querydslSourcesDir = querydslDir } // 4-3. build시 사용할 sourceSet 추가 sourceSets { main.java.srcDir querydslDir } // 4-4. querydsl 컴파일시 사용할 옵션 설정 compileQuerydsl { options.annotationProcessorPath = configurations.querydsl } // 4-5. querydsl이 compileClassPath를 상속하도록 설정 configurations { querydsl.extendsFrom compileClasspath }
- annotationProcessor는 Java 컴파일러 플러그인으로서, 컴파일 단계에서 프로젝트 내의 @Entity 어노테이션을 선언한 클래스를 탐색하고 JPAAnnotationProcessor를 통해 쿼리 타입(QClass)를 생성한다.
쿼리 타입(QClass)들은 QueryDSL을 사용하여 메서드 기반으로 쿼리를 작성할 때 프로젝트에서 만든 도메인 클래스(Entity)의 구조를 설명해주는 메타데이터 역할을 하며, 쿼리의 조건을 설정할 때 사용
QClass 생성
Gradle → other > compileQuerydsl → run
⇒ package에서 디렉터리가 생기고 QClass가 생긴다.
QuerydslConfig.java
해당 클래스를 통해 JpaQueryFactory를 bean으로 등록하여 repository에서 필요할 때마다 생성해서 쓰는 것이 아니라 바로 가져와서 사용할 수 있도록 한다.
- Querydsl을 사용하여 쿼리를 build하기 위해서는 JpaQueryFactory가 필요하다
- JpaQueryFactory를 사용하면 EntityManager를 통해 질의가 처리 된다.
import com.querydsl.jpa.impl.JPAQueryFactory; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @Configuration @EnableJpaAuditing public class QuerydslConfig { @PersistenceContext private EntityManager entityManager; public querydslConfig() { } @Bean public JPAQueryFactory jpaQueryFactory() { return new JPAQueryFactory(this.entityManager); } }
- JPAQueryFactory
- 쿼리 및 DML 절 생성을 위한 팩토리 클래스
- EntityManager를 파라미터로 넘겨줘야 한다.
User Entity / Repository
User.class
@Table(name = "user") @Entity @Getter @Builder @AllArgsConstructor @NoArgsConstructor public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "idx", nullable = false) private Long id; @Column(name = "name") private String name; @Column(name = "age") private int age; @Column(name = "email") private String email; }
UserRepository Interface
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {}
UserRepositoryCustom Interface
public interface UserRepositoryCustom { List<User> getUserList(); }
UserRepositoryImpl.class
@Repository public class UserRepositoryImpl implements UserRepositoryCustom { private final JPAQueryFactory queryFactory; public UserRepositoryImpl(JPAQueryFactory queryFactory) { this.queryFactory = queryFactory; } QUser user = QUser.user; // QUser u = new Quser("u"); - (1) @Override public List<User> getUserList() { return queryFactory .selectFrom(user) .fetch(); } }
(1) - querydsl 에서 사용하기 위한 Q-Type 생성, 파라미터로 별칭(alias)을 넘겨줄 수 있다.
UserRepository : JpaRepository를 사용하여 메서드의 이름으로 간단한 메서드들을 구현하는 용도
UserRepositoryCustom : querydsl을 통해 사용하기 위한 쿼리를 정의하는 interface
UserRepositoryImpl : Custom interface에서 정의된 메서드를 오버라이드하여 실제로 구현하는 클래스
⇒ 실제로 사용하는 곳에서는 Repository 하나만을 가지고 모든 메서드를 사용할 수 있다.
Querydsl 사용방법
Q-Type
Q-Type 인스턴스를 사용하는 2가지 방법
QUser qUser = new Quser("u"); // 별칭 직접 지정 QUser qUser = QUser.user; // 기본 인스턴스 사용
- 직접 별칭을 넣어서 사용해도 되지만 보통 기본 인스턴스에서 생성된 별칭을 사용하는 것을 권장
- 별칭 직접 사용 ⇒ 같은 테이블을 조인하는 경우 사용
검색 조건 쿼리
JPQL이 제공하는 검색 조건을 거의 대부분 제공한다.
SimpleExpression에서 제공하는 검색 조건 메서드 확인 가능하다.
user.name.eq("beomsic") // name = "beomsic" user.name.ne("beomsic") // name != "beomsic" user.name.eq("beomsic").not() // name != "beomsic" user.name.isNotNull() // name is not null user.age.in(10, 20) // age in (10, 20) user.age.notIn(10, 20) // age not in (10, 20) user.age.between(10, 30) // age between (10, 30) user.age.goe(30) // age >= 30 user.age.gt(30) // age > 30 user.age.loe(30) // age <= 30 user.age.lt(30) // age < 30 user.name.like("beomsic%") // like 검색 user.name.contains("beoms") // like '%beoms%' 검색 user.name.startsWith("beoms") // like 'beoms%' 검색
Where 문에서 AND 조건을 파라미터로 처리가 가능하다.
- where() 에 파라미터로 검색조건을 추가하면 AND 조건으로 추가 된다.
결과 조회
- fetch()
- 리스트 조회
- 데이터가 없으면 빈 리스트 반환 ( NULL ❌ )
- fetchOne()
- 단 건 조회
- 결과가 없다면 null
- 결과가 둘 이상이면 NonUniqueResultException 발생
- fetchFirst()
- limit(1).fetchOne() 과 같다.
- fetchResults()
- 페이징 정보 포함
- total count 쿼리 추가 실행
- fetchCount()
- count 쿼리로 변경하여 count 수 조회
정렬
- desc()
- 내림차순
- asc()
- 올림차순
- nullsLast()
- null 데이터 순서 부여(마지막)
- nullsFirst()
- null 데이터 순서 부여(처음)
페이징
- offset(long offset)
- 쿼리 결과에 대한 오프셋을 정의
- 0부터 시작
- limit(long limit)
- 쿼리 결과에 대한 제한/최대 결과를 정의
집합
집합 함수
- count()
- sum()
- avg()
- max()
- min()
그룹화 / 집계
- groupby()
- having()
조인
- join(), innerJoin()
- leftJoin()
- rightJoin()
- fullJoin()
JPQL의 on과 성능 최적화를 위한 fetch 조인 제공
기본 문법 : join(조인 대상, 별칭으로 사용할 Q타입)
서브 쿼리
- JPASubQuery를 생성해서 사용
- 결과가 하나면 unique(), 여러건이면 list() 사용
동적 쿼리
BooleanBuilder를 사용하면 동적 쿼리를 사용할 수 있다.
where 절에서 사용하는 데이터 값이 null인 경우 조건을 제외시킬 수 있어 동적쿼리를 만들기 편리하다.
User testUser = new User(); testUser.setName("beomsic"); testUser.setAge(26); QUser user = QUser.user; BooleanBuilder builder = new BooleanBuilder(); if (StringUtils.hasText(testUser.getName())) { builder.and(user.name.contains(testUser.getName())); } if (testUser.getAge() != null) { builder.and(user.age.gt(testUser.getAge())); } List<User> result = query.from(user) .where(builder) .list(user);
메소드 위임
메소드 위임 기능을 사용하면 쿼리 타입에 검색 조건을 직접 정의가 가능
public class UserExpression { @QueryDelegate public static BooleanExpression isExpensive(QUser user, Integer age) { return user.age.gt(age); } }
쿼리 타입에 생성된 결과
... public class QUser extends EntityPathBase<User> { ... public BooleanExpression isExpensive(Integer age) { return UserExpression.isExpensive(this, age); } }
빈 생성
쿼리 결과를 엔티티가 아닌 특정 객체(ex : dto)로 받고 싶은 경우 빈 생성 기능을 사용한다.
- 프로퍼티 접근 (setter)
- 필드 직접 접근
- 생성자 사용
UserDto.class
// DTO 생성 public class UserDTO { private String name; private int age; public UserDTO() {} public UserDTO(String name, int age) { this.name = name; this.age = age; } //Getter, Setter ... }
프로퍼티 접근 (setter)
Projections.bean
UserDTO 에 name을 가지고 있기 때문에 as를 사용해 별칭을 지정
QUser user = QUser.user; List<UserDTO> result = query.from(user).list( Projections.bean(UserDTO.class, user.name.as("name"), user.age));
필드 직접 접근
Projections.fields
필드에 직접 접근해서 값을 설정, 필드를 private로 설정해도 동작한다.
QUser user = QUser.user; List<UserDTO> result = query.from(user).list( Projections.fields(UserDTO.class, user.name.as("name"), user.age));
생성자 사용
Projections.constructor
지정한 프로젝션과 파라미터 순서가 같은 생성자가 필요하다.
QUser user = QUser.user; List<UserDTO> result = query.from(user).list( Projections.constructor(UserDTO.class, user.name.as("name"), user.age));
참고 자료
https://steady-coding.tistory.com/582
https://milenote.tistory.com/137
https://wildeveloperetrain.tistory.com/92
https://www.baeldung.com/rest-api-search-language-spring-data-querydsl
'🌱 spring' 카테고리의 다른 글
JPA - N + 1 문제 (0) 2022.10.11 영속성 컨텍스트 (0) 2022.10.11 JPA와 Hibernate, Spring data JPA (0) 2022.10.10 Spock 로 테스트 해보기 (0) 2022.10.06 MVCC (1) 2022.10.05