반응형
해당 글에서는 QueryDSL에 대해 이해하고 주요한 특징을 이해하며 처리되는 흐름에 대해 알아보기 위해 작성한 글입니다.
💡 [참고] JPA 관련해서 구성 내용에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다
분류 | 링크 |
Spring Boot Data JPA -1: ORM, JPA, Hibernate, QueryDSL 이론 | https://adjh54.tistory.com/421 |
Spring Boot Data JPA -2: 초기 환경 구성 + JpaRepository 활용 방법 | https://adjh54.tistory.com/422 |
Spring Boot Data JPA -3: 상세 JpaRepository 활용 방법(Query Method, @Query, NamedQuery) | https://adjh54.tistory.com/481 |
Spring Boot Data JPA 엔티티 어노테이션 -1 : 테이블 컬럼 단위 | https://adjh54.tistory.com/466 |
Spring Boot Data JPA 엔티티 어노테이션 -2 : 엔티티(테이블)간의 관계 | https://adjh54.tistory.com/477 |
Spring Boot Data JPA FetchType 이해하기 : 즉시/지연로딩 | https://adjh54.tistory.com/476 |
Spring Boot Data JPA + JPQL 활용 방법 | https://adjh54.tistory.com/479 |
Spring Boot Data JPA + Criteria API 활용 방법 | https://adjh54.tistory.com/483 |
Spring Boot Data JPA + QueryDSL 활용 방법-1 : 정의 및 구성요소 | https://adjh54.tistory.com/484 |
Spring Boot Data JPA + QueryDSL 활용 방법-2 : 초기 환경설정 및 활용예시 | https://adjh54.tistory.com/485 |
1) QueryDSL(Domain-Specific Languages)
💡 QueryDSL
- 다양한 데이터베이스 플랫폼(RDBMS, No-SQL, …)에 접근하여 SQL과 유사한 문법으로 쿼리를 작성하여 데이터 처리를 수행하는데 도움을 주는 프레임워크입니다.
- 타입-세이프(type-safe)하게 쿼리를 작성하도록 지원하며 SQL 형태가 아닌 ‘자바 코드’로 작성하여 데이터베이스 쿼리 작성을 쉽고 안전하게 만들어줍니다.
[ 더 알아보기 ]
💡 타입-세이프(type-safe)란?
- 프로그래밍 언어의 특성을 나타내는 용어로 프로그램이 실행되는 동안(런타임) 데이터의 타입을 체크하여 타입 오류를 방지하는 것을 의미합니다.
- 타입-세이프한 프로그래밍 언어에서는 잘못된 타입의 데이터를 사용하려고 시도하면 컴파일 오류나 런타임 오류가 발생하여 버그를 방지합니다.
1. QueryDSL 주요 특징
1.1. 타입 세이프 쿼리 생성
💡 타입 세이프 쿼리 생성
- 쿼리 작성 시 ‘컴파일 시점’에 쿼리 오류를 검출할 수 있습니다. 이는 런타임 시점에 발생할 오류를 사전에 방지하며 개발자의 생산성을 향상합니다.
- 컴파일러를 통해 쿼리 문법 오류, 필드 이름 오타, 메서드 이름 오류등을 컴파일 단계에서 발견하여 런타임에서 발생할 오류를 방지하고 코드 품질을 높이는데 도움이 됩니다.
- JPQL의 경우 String 형태로 구성된 쿼리를 이용하기에 ‘컴파일’ 단계에서 쿼리의 문제를 확인할 수 없습니다. 그러나 QueryDSL의 경우는 ‘.메서드’ 형태로 구성되어 있기에 서버를 수행하기 이전에 사전 오류를 확인할 수 있습니다.
💡 [참고] JPQL에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다
1.2. 다양한 데이터베이스 백엔드 지원
💡 다양한 데이터베이스 백엔드 지원
- 데이터베이스 백엔드는 데이터베이스 시스템에서 데이터를 저장, 검색, 업데이트, 수정하는 기능을 수행하는 부분을 말합니다.
- 이러한 데이터베이스 백엔드로 JPA, JDO, SQL, Hibernate, MongoDB, Lucene, Collection과 같은 백엔드에 대한 쿼리를 작성할 수 있도록 지원합니다.
- 이로 인해 특정 데이터베이스에 종속되지 않고 다양한 데이터베이스 환경에서 QueryDSL을 사용할 수 있습니다. 동일한 문법을 통해서 RDBMS이나 No-SQL이나 동일한 메서드를 이용하여서 쿼리를 작성합니다.
💡 RDBMS 중 MySQL을 이용한 데이터베이스 Query 구성
// 1. 구성한 JPAQueryFactory를 로드 합니다.
private final JPAQueryFactory queryFactory;
QBoardEntity board = QBoardEntity.boardEntity;
queryFactory
.select(board)
.from(board)
.where(
eqTitle(boardDto.getTitle()),
eqContent(boardDto.getContent())
)
.orderBy(board.boardSq.desc())
.fetch();
💡 NoSQL 중 MongoDB를 이용한 데이터베이스 Query 구성
// 모델 클래스인 'Person'에 대한 메타 모델 'QPerson'의 인스턴스를 생성합니다.
QPerson person = new QPerson("person");
// MongoDBQueryFactory 객체를 생성합니다. 이 객체를 사용하여 MongoDB 쿼리를 작성하고 실행합니다.
MongoDBQueryFactory queryFactory = new MongoDBQueryFactory(mongoClient, "myDatabase");
// 쿼리를 작성하고 실행합니다.
List<Person> adults = queryFactory.selectFrom(person)
.where(person.age.goe(18)) // age가 18 이상인 사람을 조회합니다.
.fetch(); // 쿼리를 실행하고 결과를 가져옵니다.
💡 [참고] MongoDB에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
1.3. 동적 쿼리 지원
💡 동적 쿼리 지원
- 동적으로 쿼리를 작성하는 것을 지원합니다. 이를 통해 프로그램 실행 중에 조건에 따라 쿼리를 변경할 수 있습니다.
💡 JpaRepositoy 예시
- JpaRepositoy의 경우는 동적쿼리를 수행할 때 복잡한 소스코드를 작성해야 하는 단점이 있었습니다.
@Repository
public interface UserJpaRepository extends JpaRepository<UserEntity, Long> {
// @Query 활용 + JPQL 활용
@Query(value = "SELECT t1 FROM UserEntity t1 WHERE t1.userId = : userId")
List<UserEntity> findByUserNm(String userNm);
// @Query 활용 + NativeQuery 활용(SQL 쿼리 활용)
@Query(value = "SELECT t1 FROM TB_USER t1 WHERE t1.userId = : userId", nativeQuery = true)
List<UserEntity> selectUserId(@Param("userId") long userId);
}
💡 MyBatis 예시
- 아래와 같이 selectBoardList를 수행하는데 파라미터로 title, content 값이 존재할 경우 WHERE 조건절로 title이나 content가 수행됩니다.
- 이렇듯 프로그램의 파라미터에 따라 실행 중에 조건이 변경되어 쿼리가 수행되는 것을 의미합니다.
<select id="selectBoardList" resultType="boardDto">
SELECT *
FROM tb_board
WHERE 1=1
<if test="title != null">
AND title = #{title}
</if>
<if test="content != null">
AND content = #{content}
</if>
</select>
💡 QueryDSL 예시
- QueryDSL의 값은 조건절(where)에서 null 값이 들어오는 경우 수행이 되지 않습니다.
- 그렇기에 파라미터 값이 존재하지 않는 경우 where절로 수행이 되지 않고, 존재하는 경우 where 절로 수행이 됩니다,
/**
* 게시판 리스트를 전체 조회합니다.
*
* @param boardDto
* @return
*/
@Override
public List<BoardEntity> selectBoardList(BoardDto boardDto) {
BooleanBuilder builder = new BooleanBuilder();
JPAQuery<BoardEntity> jpaQuery = queryFactory
.select(board)
.from(board)
.where(
eqTitle(boardDto.getTitle()),
eqContent(boardDto.getContent())
)
.orderBy(board.boardSq.desc());
}
/**
* title 값이 존재하는 경우 조건절로 추가 됩니다.
*
* @param title {String}
* @return BooleanExpression
*/
private BooleanExpression eqTitle(String title) {
if (StringUtils.isNullOrEmpty(title)) {
return null;
}
return board.title.eq(title);
}
/**
* content 값이 존재하는 경우 조건절로 추가됩니다.
*
* @param content {String}
* @return BooleanExpression
*/
private BooleanExpression eqContent(String content) {
if (StringUtils.isNullOrEmpty(content)) {
return null;
}
return board.content.eq(content);
}
1.4. 자동완성 기능 제공
💡 자동완성 기능 제공
- 쿼리를 작성하는 동안에 IDE의 자동완성 기능을 통해서 쿼리를 쉽게 작성하고 유지 관리할 수 있습니다.
- 또한 다양한 데이터베이스 백엔드 환경에서 ‘표준화된 쿼리’를 작성하여 간결하고 가독성을 높이며 오류를 줄여줍니다.
💡 엔티티를 기준으로 ‘.메서드’ 형태로 다양한 쿼리를 작성할 수 있습니다.
2. SQL, JPQL, Critera API, QueryDSL 사용예시
💡 SQL, JPQL, Critera API, QueryDSL 사용예시
- 해당 부분에서는 SQL, JPQL, Critera API, QueryDSL에서 사용되는 형태를 사용예시를 통해 알아봅니다.
💡 SQL 사용예시
- SQL문에서는 ‘테이블과 컬럼’을 대상으로 쿼리를 작성합니다.
💡 JPQL 사용예시
- JPQL에서는 SQL문과 형태가 비슷합니다. 단, ‘테이블과 컬럼’을 대상으로 하는 것이 아닌 ‘엔티티(클래스)와 변수’를 대상으로 쿼리를 작성합니다.
💡 Criteria API 사용예시
1. CriteriaBuilder 인스턴스를 생성합니다.
2. UserEntity 타입의 CriteriaBuilder를 구성합니다.
3. 데이터를 조회하는 Query의 Root를 지정합니다.
4. Root를 기준으로 전체 데이터를 조회하고 userNm의 값이 123인 값을 확인합니다.
5. 리스트 형태로 결과값을 받습니다.
💡 QueryDSL 사용예시
3. JPA 인터페이스 활용 기술들 비교
💡 JPA 인터페이스 활용 기술들 비교
- ORM(Object-Relational Mapping) 기술을 활용하여 JPA 인터페이스로 사용할 수 있는 기능들은 JpaRepository, JPQL, criteria API, QueryDSL을 이용할 수 있습니다.
분류 | JpaRepositoy | JPQL | Criteria API | QueryDSL |
타입-세이프 | 불가 | 불가 | 가능 (컴파일 타임에 오류 검출) | 가능 |
동적 쿼리 생성 | 가능 (메소드 쿼리 생성 기능) | 제한적 | 가능 (런타임 시 동적 쿼리 생성) | 가능 |
코드의 복잡성 | 간단 (Repository 인터페이스에 메소드 선언만 필요) | 간단 (직관적인 쿼리 작성 가능) | 복잡 (이해 및 유지보수가 어려울 수 있음) | 간단 |
객체 지향 쿼리 작성 | 불가 (SQL을 직접 작성) | 불가 (SQL을 직접 작성) | 가능 (객체 지향적인 방식으로 쿼리 작성 가능) | 가능 |
4. ORM 내에서 QueryDSL 구조
💡 ORM 내에서 QueryDSL 구조
- ORM(Object-relational mapping) 환경에서 QueryDSL은 JPA 구현체인 Hibernate와 함께 사용됩니다.
- JPA가 데이터베이스와의 상호 작용을 추상화하여 처리하는 반면, QueryDSL은 이러한 상호 작용을 보다 안전하고 효율적으로 작성할 수 있게 도와줍니다. 따라서 두 기술은 서로 보완적인 관계에 있습니다.
- 예를 들면, JPA의 @Entity 어노테이션을 통해 엔티티 클래스를 지정하게 되면 이를 기반으로 QueryDSL에서는 컴파일을 수행하여 Q-Class를 생성합니다. 이를 생성하여 SQL문을 구성하여 데이터 처리를 수행합니다.
2) QueryDSL(Domain-Specific Languages) 주요 개념
1. DSL(Domain-Specific Languages)
💡 DSL(Domain-Specific Languages)
- ‘특정 도메인’에 초점을 맞춘 소프트웨어 언어입니다. 이를 이용하여 특정 문제를 해결하기 위해 특정 문제 영역에 관련된 작업을 수행하는 데 사용이 됩니다.
- QueryDSL에서의 DSL은 SQL쿼리나 JPQL 쿼리와 같은 데이터베이스 쿼리를 Java에서 타입-세이프하게 작성할 수 있도록 도와줍니다. 이를 통해서 컴파일 시점에 쿼리 오류를 발견할 수 있고 IDE의 코드 자동완성 기능을 활용하여 쿼리를 쉽게 작성할 수 있습니다.
2. 메타 모델 : Q-Class
💡 메타 모델 구성 : Q-Class
- 쿼리 타입 혹은 Q-Class라는 이름으로 불리며 ‘도메인 모델의 구조’를 나타냅니다. 주로 도메인 클래스에 대한 쿼리를 작성할 때 사용되며 도메인 클래스의 각 필드를 변수로 가지고 있습니다.
- 예를 들어, UserEntity라는 엔티티 클래스가 있다면 메타 모델로 QUserEntity 클래스로 생성이 됩니다.
- 아래의 과정으로 QUserEntity 클래스가 생성됩니다.
1. 개발자는 컴파일을 수행합니다.
- build.gradle 파일 내에 작성한 태스크가 수행이 됩니다.
2. 컴파일을 수행하는 과정에서 ‘코드 생성도구 APT(Annotation Processing Tool)’가 수행이 됩니다.
- 해당 수행 과정에서 @Entity로 선언된 엔티티 클래스만 체크합니다.
- 체크가 완료되면 기존에 메타 모델(Q-Class)이 존재하였다면 비교하고 변화된 엔티티 클래스를 분석하여 메타 모델(Q-Class)을 생성 및 변경합니다.
3. 컴파일이 완료되면 QUserEntity 클래스가 생성됩니다.
3.1. 개발자는 컴파일을 수행합니다
💡 [참고] 추후에 환경 설정할 build.gradle 파일 내에서 아래와 같이 컴파일 시 수행할 Gradle Task를 지정하여 수행합니다.
3.2. 컴파일을 수행하는 과정에서 ‘코드 생성도구 APT(Annotation Processing Tool)’가 수행이 됩니다.
💡 [참고] com.querydsl:querydsl-apt 라이브러리를 이용하여 소스코드를 생성합니다.
3.3. 컴파일이 완료되면 QUserEntity 클래스가 생성됩니다.
3. JDO(Java Data Objects)
💡 JDO(Java Data Objects)
- 자바 애플리케이션에서 데이터베이스에 저장된 데이터에 접근하고 조작하는 API입니다.
- JDO는 객체 지향적인 접근을 제공하므로, 개발자는 SQL 쿼리를 직접 작성하지 않고도 데이터베이스의 데이터에 접근할 수 있습니다.
💡 JDO 사용예시
- 자바 데이터 객체(Java Data Objects, JDO)를 사용하여 데이터베이스에서 데이터를 조회하는 예시입니다.
1. 메타 모델인 QCustomer의 인스턴스를 생성합니다. 이 인스턴스를 통해 쿼리 구성에 필요한 모든 필드와 메서드에 접근 가능합니다.
2. JDOQuery 객체를 생성합니다. 이 객체를 사용하여 데이터베이스 쿼리를 작성하고 실행합니다. pm은 PersistenceManager의 인스턴스로, 데이터베이스와의 모든 상호작용을 관리합니다.
3. 쿼리를 작성하여 데이터를 조회합니다. 여기서는 customer 테이블의 모든 필드를 선택하며 테이블을 지정하고 조건절로 customer 테이블의 firstName 필드가 "Bob"인 레코드를 찾습니다.
- 최종적으로 .fetchOne(); : 쿼리를 실행하고 결과를 가져옵니다. 이 메서드는 단일 결과를 반환합니다.
4. 쿼리 실행이 끝난 후에는 반드시 Query 객체를 닫아야 합니다. 이를 통해 사용한 리소스를 해제하고 데이터베이스 연결을 종료합니다.
// 1
QCustomer customer = QCustomer.customer;
// 2
JDOQuery<?> query = new JDOQuery<Void>(pm);
// 3
Customer bob = query.select(customer)
.from(customer)
.where(customer.firstName.eq("Bob"))
.fetchOne();
// 4
query.close();
3) QueryDSL 구성요소
1. QueryDSL 수행 과정
💡 QueryDSL 수행 과정
- QueryDSL을 수행하는 과정에서 이용되는 구성요소에 대해 알아봅니다.
1. 서버 컴파일
- 서버는 엔티티 클래스를 기반으로 APT (Annotation Processing Tool)를 사용하여 컴파일을 수행합니다. 이는 컴파일 시점에 자동으로 발생합니다.
- 엔티티 클래스에 변화가 발생하면, 빌드를 다시 수행하여 메타 모델 (Q-Class)과 동기화를 맞춥니다.
2. JPAQueryFactory 구성
- JPAQueryFactory을 설정합니다. 이는 QueryDSL을 사용하는 데 필요한 클래스로, JPAQuery의 인스턴스를 생성합니다.
3. 메타 모델 구성 : Q-Class
- Q-Class를 설정합니다. 이는 도메인 모델의 구조를 나타내는 Query Type으로 알려져 있습니다.
- 이는 주로 도메인 클래스를 기반으로 쿼리를 작성하는 데 사용됩니다. 도메인 클래스의 각 필드는 Q-Class의 변수입니다.
4. 쿼리문 구성 : JPAQuery
- JPAQuery를 설정합니다. 이 클래스는 실제 쿼리를 생성하고 실행하는 데 사용됩니다. 'where', 'orderBy', 'groupBy' 등의 메서드를 사용하여 쿼리를 구성할 수 있습니다.
5. 특정 필드 조회 : Projection
- 엔티티 내에 특정 필드 값만 조회하는 방법을 Projection을 사용합니다. 이를 통해 필요한 정보만 효율적으로 가지고 올 수 있습니다.
- Tuple, Projections.field, Projections.beans, Projections.constructor, @QueryProjection를 이용하여 각각 필드에 맞는 값을 조회할 수 있습니다.
6. 동적 조건절 구성 : BooleanExpression
- BooleanExpression을 설정합니다. 이 클래스는 QueryDSL의 조건 절을 나타냅니다. 'and', 'or', 'not' 등의 메서드를 제공합니다.
6. 결과값 구성: fetchable
- QueryResults를 설정합니다. 이 클래스는 쿼리 실행 결과를 보유하고 있습니다. 결과 목록뿐만 아니라 총 결과 수, 페이지 정보 등도 제공합니다.
2. JPAQueryFactory 구성
💡 JPAQueryFactory
- QueryDSL을 사용하는데 필요한 주요 클래스입니다. 이 클래스는 JPAQuery의 인스턴스를 생성하는 역할을 합니다.
- 이를 통해 실제 데이터베이스에 대한 쿼리를 작성하고 실행할 수 있습니다. 즉, JPAQueryFactory는 데이터베이스 쿼리를 생성하고 실행하는 메커니즘을 제공하는 중요한 구성 요소입니다.
@Configuration
@EnableJpaAuditing
public class JPAConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
💡 [참고] QueryDSL 5.1.0 기준 JPAQueryFactory 생성자
생성자 | 설명 |
JPAQueryFactory(JPQLTemplates templates, Supplier<javax.persistence.EntityManager> entityManager) | JPQL 템플릿과 엔티티 매니저 공급자를 이용해 쿼리 팩토리를 생성 |
JPAQueryFactory(JPQLTemplates templates, javax.persistence.EntityManager entityManager) | JPQL 템플릿과 엔티티 매니저를 이용해 쿼리 팩토리를 생성 |
JPAQueryFactory(Supplier<javax.persistence.EntityManager> entityManager) | 엔티티 매니저 공급자를 이용해 쿼리 팩토리를 생성 |
JPAQueryFactory(javax.persistence.EntityManager entityManager) | 엔티티 매니저를 이용해 쿼리 팩토리를 생성 |
💡 [참고] QueryDSL 5.1.0 기준 JPAQueryFactory 메서드
메서드 | 리턴 값 | 설명 |
delete(EntityPath<?> path) | JPADeleteClause | 새로운 DELETE 절을 생성 |
from(EntityPath<?> from) | JPAQuery<?> | 주어진 소스로 새 쿼리 생성 |
from(EntityPath<?>... from) | JPAQuery<?> | 주어진 소스로 새 쿼리 생성 |
insert(EntityPath<?> path) | JPAInsertClause | 새로운 INSERT 절을 생성 |
query() | JPAQuery<?> | |
select(Expression<?>... exprs) | JPAQuery<Tuple> | 주어진 프로젝션으로 새 JPQLQuery 인스턴스 생성 |
select(Expression<T> expr) | <T> JPAQuery<T> | 주어진 프로젝션으로 새 JPQLQuery 인스턴스 생성 |
selectDistinct(Expression<?>... exprs) | JPAQuery<Tuple> | 주어진 프로젝션으로 새 JPQLQuery 인스턴스 생성 |
selectDistinct(Expression<T> expr) | <T> JPAQuery<T> | 주어진 프로젝션으로 새 JPQLQuery 인스턴스 생성 |
selectFrom(EntityPath<T> from) | <T> JPAQuery<T> | 주어진 소스 및 프로젝션으로 새 JPQLQuery 인스턴스 생성 |
selectOne() | JPAQuery<Integer> | 프로젝션을 이용하여 하나의 새 JPQLQuery 인스턴스 생성 |
selectZero() | JPAQuery<Integer> | 프로젝션을 이용하여 0개의 새 JPQLQuery 인스턴스 생성 |
update(EntityPath<?> path) | JPAUpdateClause | 새로운 UPDATE 절을 생성 |
3. 메타 모델 구성 : Q-Class
💡 메타 모델 구성 : Q-Class
- 쿼리 타입 혹은 Q-Class라는 이름으로 불리며 ‘도메인 모델의 구조’를 나타냅니다. 주로 도메인 클래스에 대한 쿼리를 작성할 때 사용되며 도메인 클래스의 각 필드를 변수로 가지고 있습니다.
- 예를 들어 UserEntity라는 엔티티 클래스가 있다면 메타 모델로 QUserEntity 클래스로 생성이 됩니다.
- QueryDSL의 코드 생성 도구를 통해 자동으로 생성될 수 있습니다. 일반적으로 컴파일을 수행하는 단계에 변화된 엔티티를 분석하여 해당하는 Q-Class를 생성해 줍니다.
- 이렇게 생성된 Q-Class를 사용하면 쿼리 작성 시 컴파일 타임에 오류를 잡을 수 있으며 IDE의 코드 자동완성 기능을 활용할 수 있어 편리합니다.
- 하지만 Q-Class는 도메인 클래스의 구조가 변경될 때마다 재생성해야 합니다. 그렇지 않으면, 도메인 클래스와 Q-Class 사이에 불일치가 생기게 되어 쿼리 오류를 일으킬 수 있습니다.
💡 컴파일이 수행되면 엔티티 클래스와 매핑하는 Q-Class를 아래와 같이 생성해 줍니다.
- 개발자는 엔티티에 직접 접근하는 것이 아닌 Q-Class에 접근하여서 개발을 수행합니다.
💡 엔티티 클래스와 Q-Class 비교
- 엔티티 클래스를 기반으로 설정에서 지정한 build compile을 수행하면 아래와 같이 UserEntity.java를 기반으로 QUserEntity.java 파일이 생성됩니다.
💡 Q-Class 구성 : 메타 모델
- 메타 모델인 Q-Class를 인스턴스화하는 과정입니다.
- 'QBoardEntity.boardEntity'는 QueryDSL에서 생성된 도메인 클래스의 Q-Class입니다. 이 인스턴스를 통해 쿼리 구성에 필요한 모든 필드와 메서드에 접근 가능합니다.
@Repository
public class BoardDaoImpl implements BoardDao {
// 1. 구성한 JPAQueryFactory를 로드 합니다.
private final JPAQueryFactory queryFactory;
public BoardDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 게시판 리스트를 전체 조회합니다.
*
* @param boardDto
* @return
*/
@Override
public List<BoardEntity> selectBoardList(BoardDto boardDto) {
QBoardEntity board = QBoardEntity.boardEntity;
}
}
4. 쿼리문 구성 : JPAQuery
💡 쿼리문 구성 : JPAQuery
- QueryDSL에서 제공하는 중요한 클래스로 실제 데이터베이스에 대한 쿼리를 작성하고 실행하는 역할을 합니다.
- JPAQuery에서는 실제 데이터베이스를 수행하기 위한 쿼리로 .select, .where, .orderBy, .groupBy 등의 메서드를 통해 쿼리를 구성하는 데 사용이 됩니다.
@Repository
public class BoardDaoImpl implements BoardDao {
// 1. 구성한 JPAQueryFactory를 로드 합니다.
private final JPAQueryFactory queryFactory;
public BoardDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 게시판 리스트를 전체 조회합니다.
*
* @param boardDto
* @return
*/
@Override
public List<BoardEntity> selectBoardList(BoardDto boardDto) {
// 데이터베이스와 매핑된 boardEntity 클래스를 Q-Class
QBoardEntity board = QBoardEntity.boardEntity;
JPAQuery<BoardEntity> jpaQuery = queryFactory
.select(board)
.from(board)
.where(board.boardSq.eq(boardDto.getBoardSq()))
.orderBy(board.boardSq.desc());
}
}
💡 JPAQuery 클래스의 주요 사용 메서드
- 해당 클래스는 아래의 패키지로부터 상속을 받아 모두 사용할 수 있습니다.
- com.querydsl.jpa.JPAQueryBase
-com.querydsl.core.support.FetchableSubQueryBase
- com.querydsl.core.support.QueryBase
- com.querydsl.core.support.ExtendedSubQuery
메서드 | 패키지 클래스 | 설명 |
select | JPQLQueryFactory | 조회할 컬럼을 지정 |
from | JPQLQueryFactory | 조회할 테이블을 지정 |
selectFrom | JPQLQueryFactory | 조회할 테이블과 컬럼을 한번에 지정 |
selectDistinct | JPQLQueryFactory | 중복을 제거한 결과를 조회 |
insert | JPQLQueryFactory | 새로운 레코드 삽입 |
delete | JPQLQueryFactory | 레코드 삭제 |
distinct | QueryBase | 중복 제거 |
where | QueryBase | 특정 조건으로 데이터를 필터 |
orderBy | QueryBase | 결과를 특정 기준으로 정렬 |
groupBy | QueryBase | 특정 컬럼으로 그룹화 |
having | QueryBase | 그룹화된 결과에 추가 필터 적용 |
limit | QueryBase | 결과의 최대 개수를 제한 |
offset | QueryBase | 시작점을 지정하여 결과의 일부분만 반환 |
innerJoin | JPAQueryBase | 두 테이블의 교집합을 반환 |
fetchJoin | JPAQueryBase | 연관된 엔티티를 함께 조회 |
leftJoin | JPAQueryBase | 왼쪽 테이블의 모든 데이터와 오른쪽 테이블의 일치하는 데이터를 반환 |
💡 [참고] 추가 메서드에 대해 궁금하시면 아래의 링크를 참고하시면 도움이 됩니다.
5. 특정 필드 조회 : Projection
💡 특정 필드 조회 : Projection
- 엔티티 내에 특정 필드 값만 조회하는 방법을 Projection을 사용합니다. 이를 통해 필요한 정보만 효율적으로 가지고 올 수 있습니다.
- Tuple, Projections.field, Projections.beans, Projections.constructor, @QueryProjection를 이용하여 각각 필드에 맞는 값을 조회할 수 있습니다.
QBoardEntity board = QBoardEntity.boardEntity;
List<Tuple> result = queryFactory
.select(board.boardName, board.boardContent)
.from(board)
.fetch();
💡 [참고] 특정 필드 조회 : Projection API Document
메서드 | 리턴 값 | 설명 |
appending(Expression<T> base, Expression<?>... rest) | static <T> com.querydsl.core.types.AppendingFactoryExpression<T> | 모든 인수를 직렬화하지만 기본값을 반환값으로 사용하는 appending factory expression을 생성합니다 |
array(Class<T[]> type, Expression<T>... exprs) | static <T> ArrayConstructorExpression<T> | 주어진 유형 및 표현식에 대한 타입 배열 프로젝션을 생성합니다 |
bean(Path<? extends T> type, Expression<?>... exprs) | static <T> QBean<T> | 주어진 유형 및 표현식에 대해 Bean을 채우는 프로젝션을 생성합니다 |
bean(Path<? extends T> type, Map<String,? extends Expression<?>> bindings) | static <T> QBean<T> | 주어진 유형 및 바인딩에 대해 Bean을 채우는 프로젝션을 생성합니다 |
bean(Class<? extends T> type, Expression<?>... exprs) | static <T> QBean<T> | 주어진 유형 및 표현식에 대해 Bean을 채우는 프로젝션을 생성합니다 |
bean(Class<? extends T> type, Map<String,? extends Expression<?>> bindings) | static <T> QBean<T> | 주어진 유형 및 바인딩에 대해 Bean을 채우는 프로젝션을 생성합니다 |
constructor(Class<? extends T> type, Expression<?>... exprs) | static <T> ConstructorExpression<T> | 주어진 유형 및 표현식에 대한 생성자 호출 프로젝션을 생성합니다 |
constructor(Class<? extends T> type, Class<?>[] paramTypes, Expression<?>... exprs) | static <T> ConstructorExpression<T> | 주어진 유형, 매개변수 유형 및 표현식에 대한 생성자 호출 프로젝션을 생성합니다 |
fields(Path<? extends T> type, Expression<?>... exprs) | static <T> QBean<T> | 주어진 유형 및 표현식에 대해 필드 접근 기반 Bean 채우기 프로젝션을 생성합니다 |
fields(Path<? extends T> type, Map<String,? extends Expression<?>> bindings) | static <T> QBean<T> | 주어진 유형 및 바인딩에 대해 필드 접근 기반 Bean 채우기 프로젝션을 생성합니다 |
fields(Class<? extends T> type, Expression<?>... exprs) | static <T> QBean<T> | 주어진 유형 및 표현식에 대해 필드 접근 기반 Bean 채우기 프로젝션을 생성합니다 |
fields(Class<? extends T> type, Map<String,? extends Expression<?>> bindings) | static <T> QBean<T> | 주어진 유형 및 바인딩에 대해 필드 접근 기반 Bean 채우기 프로젝션을 생성합니다 |
list(Expression<?>... args) | static QList | 주어진 표현식에 대한 새로운 List 유형 프로젝션을 생성합니다 |
list(Expression<?>[]... args) | static QList | 주어진 표현식에 대한 새로운 List 유형 프로젝션을 생성합니다 |
list(List<Expression<?>> args) | static QList | 주어진 표현식에 대한 새로운 List 유형 프로젝션을 생성합니다 |
map(Expression<?>... exprs) | static QMap | 주어진 표현식에 대한 Map 유형 프로젝션을 생성합니다 |
tuple(Expression<?>... exprs) | static QTuple | 주어진 표현식에 대한 Tuple 유형 프로젝션을 생성합니다 |
tuple(Expression<?>[]... exprs) | static QTuple | 주어진 표현식에 대한 Tuple 유형 프로젝션을 생성합니다 |
tuple(List<Expression<?>> exprs) | static QTuple | 주어진 표현식에 대한 Tuple 유형 프로젝션을 생성합니다 |
💡 Projection을 이용하는 방법에 대해 간단히 요약을 확인해봅니다.
타입/메서드/어노테이션 | 설명 |
tuple | 여러 컬럼의 값을 한 번에 가져올 수 있는 구조입니다. 각 컬럼의 값들을 한번에 담을 수 있는 역할을 합니다. |
Projections.field | 주어진 표현에 대한 프로젝션을 생성하는 데 사용됩니다. 이 메서드는 특정 필드만 선택하여 조회하는 방법으로 사용되며, 이를 통해 필요한 정보만 효율적으로 가지고 올 수 있습니다. |
Projections.beans | 주어진 표현식에 대해 Bean을 채우는 프로젝션을 생성하는 데 사용됩니다. 이 메소드는 원하는 필드만을 선택하여 특정 Bean에 담아 반환하는 방법으로 사용되며, 이를 통해 필요한 정보만 효율적으로 가지고 올 수 있습니다. |
Projections.constructor | 주어진 유형과 표현식을 이용해 생성자 호출 프로젝션을 생성하는 데 사용됩니다. 이 메소드는 원하는 필드를 선택하여 특정 클래스의 생성자에 매핑하여 객체를 생성하는 방법으로 사용됩니다. 이를 통해 필요한 정보만을 선택적으로 가지고 와서 특정 객체를 효율적으로 생성할 수 있습니다. |
@QueryProjection | Querydsl이 제공하는 어노테이션으로, 이를 생성자에 붙여주면 해당 생성자를 이용한 조회를 간편하게 할 수 있습니다. 이를 통해 필요한 정보만을 선택적으로 가지고 와서 특정 객체를 효율적으로 생성할 수 있습니다. |
5.1. 다중 컬럼 : tuple
💡 다중 컬럼 : tuple
- 여러 컬럼의 값을 한 번에 가져올 수 있는 구조입니다. 즉, Tuple은 다수의 각기 다른 타입의 값들을 하나의 단위로 묶어서 처리할 수 있게 해 줍니다.
- 특히, select 절에서 여러 컬럼을 지정하였을 때, 각 컬럼의 값들을 한번에 담을 수 있는 역할을 합니다. 이를 통해, 데이터베이스로부터 복수의 컬럼 값을 조회하였을 때, 각 컬럼의 값을 개별적인 변수가 아닌, Tuple이라는 하나의 단위로 처리할 수 있습니다.
-이렇게 Tuple을 사용하면, 각 컬럼의 값을 별도로 처리하는 것이 아니라, 하나의 단위로 묶어서 처리할 수 있으므로 코드의 가독성을 향상시키고, 코드의 복잡성을 줄일 수 있습니다.
💡 다중 컬럼 : tuple 사용예시
- 여기서 select 절에서 board.boardId, board.title 두 개의 컬럼을 지정하였습니다. 결과값으로 Tuple 값을 반환합니다.
@Repository
public class BoardDaoImpl implements BoardDao {
// 1. 구성한 JPAQueryFactory를 로드 합니다.
private final JPAQueryFactory queryFactory;
public BoardDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 게시판의 아이디와 제목만을 조회합니다.
*
* @param boardId
* @return
*/
@Override
public Tuple getBoardDetail(Long boardId) {
QBoardEntity board = QBoardEntity.boardEntity;
// 여기서 select 절에서 board.boardId, board.title 두 개의 컬럼을 지정하였습니다.
Tuple result = queryFactory
.select(board.boardId, board.title)
.from(board)
.where(board.boardId.eq(boardId))
.fetchOne();
return result;
}
}
5.2. Projections.field()
💡 Projections.field()
- 주어진 표현에 대한 프로젝션을 생성하는 데 사용됩니다. 이 메서드는 특정 필드만 선택하여 조회하는 방법으로 사용되며, 이를 통해 필요한 정보만 효율적으로 가지고 올 수 있습니다.
메서드 | 리턴 값 | 설명 |
fields(Path<? extends T> type, Expression<?>... exprs) | static <T> QBean<T> | 주어진 유형 및 표현식에 대해 필드 접근 기반 Bean 채우기 프로젝션을 생성합니다 |
fields(Path<? extends T> type, Map<String,? extends Expression<?>> bindings) | static <T> QBean<T> | 주어진 유형 및 바인딩에 대해 필드 접근 기반 Bean 채우기 프로젝션을 생성합니다 |
fields(Class<? extends T> type, Expression<?>... exprs) | static <T> QBean<T> | 주어진 유형 및 표현식에 대해 필드 접근 기반 Bean 채우기 프로젝션을 생성합니다 |
fields(Class<? extends T> type, Map<String,? extends Expression<?>> bindings) | static <T> QBean<T> | 주어진 유형 및 바인딩에 대해 필드 접근 기반 Bean 채우기 프로젝션을 생성합니다 |
💡 Projections.field() 사용예시
- BoardDto라는 DTO 클래스에 board.boardId와 board.title 두 필드를 Projection.field를 이용하여 담아 반환합니다. 이렇게 하면 원하는 필드만을 효율적으로 조회할 수 있습니다.
@Repository
public class BoardDaoImpl implements BoardDao {
// 1. 구성한 JPAQueryFactory를 로드 합니다.
private final JPAQueryFactory queryFactory;
public BoardDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 게시판의 아이디와 제목만을 조회합니다.
*
* @param boardId
* @return
*/
@Override
public BoardDto getBoardDetail(Long boardId) {
QBoardEntity board = QBoardEntity.boardEntity;
// 여기서 select 절에서 board.boardId와 board.title 두 개의 컬럼을 지정하였습니다.
BoardDto result = queryFactory
.select(Projections.fields(BoardDto.class, board.boardId, board.title))
.from(board)
.where(board.boardId.eq(boardId))
.fetchOne();
return result;
}
}
5.3. Projections.beans()
💡 Projections.beans()
- 주어진 표현식에 대해 Bean을 채우는 프로젝션을 생성하는 데 사용됩니다. 이 메서드는 원하는 필드만을 선택하여 특정 Bean에 담아 반환하는 방법으로 사용되며, 이를 통해 필요한 정보만 효율적으로 가지고 올 수 있습니다.
메서드 | 리턴 값 | 설명 |
bean(Class<? extends T> type, Expression<?>... exprs) | static <T> QBean<T> | 주어진 유형 및 표현식에 대해 Bean을 채우는 프로젝션을 생성합니다 |
bean(Class<? extends T> type, Map<String,? extends Expression<?>> bindings) | static <T> QBean<T> | 주어진 유형 및 바인딩에 대해 Bean을 채우는 프로젝션을 생성합니다 |
💡Projections.beans() 사용예시
- BoardDto라는 DTO 클래스에 board.boardId와 board.title 두 필드를 Projection.beans를 이용하여 담아 반환합니다. 이렇게 하면 원하는 필드만을 효율적으로 조회할 수 있습니다.
@Repository
public class BoardDaoImpl implements BoardDao {
// 1. 구성한 JPAQueryFactory를 로드 합니다.
private final JPAQueryFactory queryFactory;
public BoardDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 게시판의 아이디와 제목만을 조회합니다.
*
* @param boardId
* @return
*/
@Override
public BoardDto getBoardDetail(Long boardId) {
QBoardEntity board = QBoardEntity.boardEntity;
// 여기서 select 절에서 board.boardId와 board.title 두 개의 컬럼을 지정하였습니다.
BoardDto result = queryFactory
.select(Projections.bean(BoardDto.class, board.boardId, board.title))
.from(board)
.where(board.boardId.eq(boardId))
.fetchOne();
return result;
}
}
5.4. Projections.constructor()
💡 Projections.constructor()
- 주어진 유형과 표현식을 이용해 생성자 호출 프로젝션을 생성하는 데 사용됩니다. 이 메소드는 원하는 필드를 선택하여 특정 클래스의 생성자에 매핑하여 객체를 생성하는 방법으로 사용됩니다.
- 이를 통해 필요한 정보만을 선택적으로 가지고 와서 특정 객체를 효율적으로 생성할 수 있습니다.
메서드 | 리턴 값 | 설명 |
constructor(Class<? extends T> type, Expression<?>... exprs) | static <T> ConstructorExpression<T> | 주어진 유형과 표현식에 대한 생성자 호출 프로젝션을 생성합니다 |
constructor(Class<? extends T> type, Class<?>[] paramTypes, Expression<?>... exprs) | static <T> ConstructorExpression<T> | 주어진 유형, 매개변수 유형, 표현식에 대한 생성자 호출 프로젝션을 생성합니다 |
💡 Projections.constructor 사용 예시
- BoardDto 클래스의 생성자를 호출하여 주어진 board.boardId와 board.title 컬럼의 값을 매개변수로 넘겨 BoardDto 객체를 생성하고 반환합니다.
@Repository
public class BoardDaoImpl implements BoardDao {
// 1. 구성한 JPAQueryFactory를 로드 합니다.
private final JPAQueryFactory queryFactory;
public BoardDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 게시판의 아이디와 제목만을 조회합니다.
*
* @param boardId
* @return
*/
@Override
public BoardDto getBoardDetail(Long boardId) {
QBoardEntity board = QBoardEntity.boardEntity;
// 여기서 select 절에서 board.boardId와 board.title 두 개의 컬럼을 지정하였습니다.
BoardDto result = queryFactory
.select(Projections.constructor(BoardDto.class, board.boardId, board.title))
.from(board)
.where(board.boardId.eq(boardId))
.fetchOne();
return result;
}
}
5.5. 생성자 사용: @QueryProjection
💡 생성자 사용: @QueryProjection
- Querydsl이 제공하는 어노테이션으로, 이를 생성자에 붙여주면 해당 생성자를 이용한 조회를 간편하게 할 수 있습니다. 이를 통해 필요한 정보만을 선택적으로 가지고 와서 특정 객체를 효율적으로 생성할 수 있습니다.
💡 @QueryProjection 사용 예시
- 아래의 코드 예시는 BoardDto 클래스에 @QueryProjection 어노테이션을 붙인 생성자를 이용하여 board.boardId와 board.title 컬럼의 값을 매개변수로 넘겨 BoardDto 객체를 생성하고 반환하는 예시입니다.
public class BoardDto {
private Long boardId;
private String title;
@QueryProjection
public BoardDto(Long boardId, String title) {
this.boardId = boardId;
this.title = title;
}
// getters and setters
}
@Repository
public class BoardDaoImpl implements BoardDao {
// 1. 구성한 JPAQueryFactory를 로드 합니다.
private final JPAQueryFactory queryFactory;
public BoardDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 게시판의 아이디와 제목만을 조회합니다.
*
* @param boardId
* @return
*/
@Override
public BoardDto getBoardDetail(Long boardId) {
QBoardEntity board = QBoardEntity.boardEntity;
// 여기서 select 절에서 board.boardId와 board.title 두 개의 컬럼을 지정하였습니다.
BoardDto result = queryFactory
.select(new QBoardDto(board.boardId, board.title))
.from(board)
.where(board.boardId.eq(boardId))
.fetchOne();
return result;
}
}
6. 결과값 구성: fetchable
💡 결과값 구성: fetchable
- QueryDSL에서 제공하는 인터페이스로, 쿼리의 결과를 가져오는 메서드를 정의하고 있습니다. 이는 주로 QueryDSL 쿼리의 마지막 부분에서 사용되며, 쿼리를 실행하고 그 결과를 반환합니다.
메서드 | 리턴 값 | 결과 | 설명 |
fetch() | List<T> | 여러개 반환 값 | 쿼리를 실행하여 결과를 리스트로 가져옵니다. 결과가 없는 경우에는 빈 리스트를 반환합니다. |
fetchCount() | long | 결과의 수 | count 쿼리를 실행하여 결과 수를 가져옵니다. |
fetchOne() | T | 단일 반환 값 | 쿼리를 실행하여 단일 결과를 가져옵니다. 결과가 없는 경우에는 null을 반환합니다. 결과가 둘 이상인 경우에는 NonUniqueResultException을 발생시킵니다. |
fetchFirst() | T | 단일 반환 값(첫번째) | 쿼리를 실행하여 첫 번째 결과를 가져옵니다. 결과가 없는 경우에는 null을 반환합니다. |
fetchResults() | QueryResults<T> | 전체 결과 수, 페이지 정보 등.. | 쿼리를 실행하여 결과와 함께 전체 결과 수를 포함하는 QueryResults 객체를 가져옵니다. |
💡 결과값 구성: fetchable 사용예시
- selectBoardList() 메서드의 반환 값으로 아래와 같이 구성하였고, .fetch()를 통해서 리스트 형태의 결과 값을 반환받습니다.
@Repository
public class BoardDaoImpl implements BoardDao {
// 1. 구성한 JPAQueryFactory를 로드 합니다.
private final JPAQueryFactory queryFactory;
public BoardDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
// 데이터베이스와 매핑된 boardEntity 클래스를 Q-Class
QBoardEntity board = QBoardEntity.boardEntity;
/**
* 게시판 리스트를 전체 조회합니다.
*
* @param boardDto
* @return
*/
@Override
public List<BoardEntity> selectBoardList(BoardDto boardDto) {
return queryFactory
.select(board)
.from(board)
.where(
eqTitle(boardDto.getTitle()),
eqContent(boardDto.getContent())
)
.orderBy(board.boardSq.desc())
.fetch();
}
/**
* title 값이 존재하는 경우 조건절로 추가 됩니다.
*
* @param title {String}
* @return BooleanExpression
*/
private BooleanExpression eqTitle(String title) {
if (StringUtils.isNullOrEmpty(title)) {
return null;
}
return board.title.eq(title);
}
/**
* content 값이 존재하는 경우 조건절로 추가됩니다.
*
* @param content {String}
* @return BooleanExpression
*/
private BooleanExpression eqContent(String content) {
if (StringUtils.isNullOrEmpty(content)) {
return null;
}
return board.content.containsIgnoreCase(content);
}
}
오늘도 감사합니다. 😀
반응형