Spring/Spring Data JPA
[자바 ORM 표준 JPA 프로그래밍] 10-3. 객체지향 쿼리 언어: QueryDSL
noahkim_
2024. 8. 2. 11:11
김영한 님의 "자바 ORM 표준 JPA 프로그래밍" 책을 정리한 포스팅 입니다.
1. QueryDSL
- Java에서 쿼리를 생성할 수 있게 해주는 오픈소스 프레임워크
특징
항목 | 설명 |
타입 안전성 |
자바 코드 기반
- 컴파일 시점에 타입 체크 |
유연성 |
동적 쿼리 생성
- 주어진 조건에 따라 동적으로 JPQL 쿼리를 생성하여 실행함 |
Fluent API |
Method Chaining 방식
- 직관적이고 가독성이 높은 쿼리 작성 |
다양한 지원 |
다양한 데이터베이스 및 기술 지원 (SQL, JPA, MongoDB, Lucene 등)
|
예시) 타입 안전성
더보기
@Test
@Transactional
void type_safety() {
QUser user = QUser.user;
JPAQuery<User> query = new JPAQuery<>(entityManager);
User response = query.select(user)
.from(user)
.where(
user.name.eq("updated").and(user.id.lt(10)) // 필드 & 인자값의 타입 안전이 보장됨
).fetchOne();
assertEquals(response.getName(), "updated");
}
예시) 유연성
더보기
@Test
@Transactional
void flexibility_test() {
QUser user = QUser.user;
JPAQuery<User> query = new JPAQuery<>(entityManager);
query.select(user).from(user);
String searchName = "updated";
Integer minId = 10;
// AND 조건 누적을 위한 BooleanBuilder 사용
BooleanBuilder builder = new BooleanBuilder();
if (searchName != null) builder.and(user.name.eq(searchName));
if (minId != null) builder.and(user.id.lt(minId));
User response = query.where(builder).fetchOne();
assertEquals(response.getName(), "updated");
}
예시) Fluent API
더보기
QUser user = QUser.user;
JPAQuery<User> query = new JPAQuery<>(entityManager);
List<User> users = query.select(user)
.from(user)
.where(user.id.lt(10))
.orderBy(user.name.asc())
.limit(10)
.fetch();
vs Criteria API
항목 | Criteria API | QueryDSL |
기본 스타일 | JPA 표준 스펙 |
서드파티 라이브러리 (open-source)
|
코드 가독성 | ❌ 복잡하고 장황 |
✅ 간결하고 직관적
|
타입 안전성 | ✅ 지원 |
✅ 지원 (더 자연스러움)
|
IDE 자동완성 | ❌ 한계 있음 |
✅ 깔끔한 자동완성
|
런타임 오류 가능성 | ✅ 낮음 | ✅ 낮음 |
동적 쿼리 | ❌ 코드 복잡해짐 |
✅ BooleanBuilder 등으로 자연스럽게 작성 가능
|
JPQL 기반 쿼리 작성 | ✅ 지원 (CriteriaQuery) |
✅ 지원 (JPAQuery, QueryFactory)
|
복잡한 쿼리 작성 난이도 | ❌ 높음 |
✅ 상대적으로 쉬움
|
러닝 커브 | ⬆ 초반 진입장벽 낮음 (표준이라 문서 많음) |
⬆ 초반에는 설정이 필요함 (Q타입 생성 등)
|
프레임워크 통합 | ✅ JPA 표준이라 어디서나 사용 가능 |
🔶 별도 설정 필요 (Gradle/Maven에서 Q타입 생성 등)
|
2. 셋팅
dependencies {
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
def querydslDir = "src/main/generated"
sourceSets {
main.java.srcDirs += [ querydslDir ]
}
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}
clean.doLast {
file(querydslDir).deleteDir()
}
3. 컴포넌트
항목 | Q Class |
JPAQuery / JPAQueryFactory
|
역할 | 엔티티 기반 메타데이터 클래스 (필드 정보 포함) |
타입 안전한 쿼리 생성 및 실행
|
생성 방식 | 컴파일 시 자동 생성 (querydsl-apt 플러그인) |
직접 인스턴스 생성 (new JPAQuery<>, new JPAQueryFactory(...))
|
용도 | 쿼리에서 사용할 엔티티 필드 참조 |
select, from, where, fetch 등 쿼리 구성
|
타입 안전성 | 필드에 대한 타입 안전성 제공 |
쿼리 전반에 걸쳐 타입 안전성 제공
|
특징 | - 엔티티 필드 정보를 메타데이터로 표현 - IDE 자동완성 지원 |
- Fluent API
- 동적 조건 처리 용이 - 메서드 체이닝으로 직관적인 쿼리 구성 |
예제
더보기
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
@PersistenceContext
private EntityManager em;
@Override
public List<User> findByCustomPredicate(String firstname) {
JPAQuery query = new JPAQuery(em);
// Q Class
QMember member = QMember.member;
// JPA Query
List<Member> members = query.from(member)
.where(member.username.eq(firstname))
.list(member);
}
}
4. 기능
주제 | 설명 |
조회 | fetch 조회 지원 |
조인 | 엔티티 간 관계를 SQL Join처럼 연결 |
서브쿼리 | 쿼리 안에 쿼리 작성 |
프로젝션 및 결과 반환 | 컬럼 지정 조회 및 DTO 매핑 |
수정/삭제 쿼리 | 업데이트/삭제 배치 처리 |
동적 쿼리 | 조건에 따라 동적으로 where 절 생성 |
메서드 위임 | 동적 조건을 메서드화 하여 재사용 |
예제) 조회
더보기
메서드 | 결과가 1건일 때 | 결과가 여러 건일 때 | 결과가 없을 때 | 반환 형태 |
fetchOne() | ✅ 정상 반환 | ❌ NonUniqueResultException | ❌ null | 단일 객체 |
fetch() | ✅ 리스트 반환 | ✅ 전체 리스트 반환 | ✅ 빈 리스트 반환 | List<T> |
fetchOne()
@Test
@Transactional
void unique_list_success() {
QUser user = QUser.user;
JPAQuery<User> query = new JPAQuery<>(entityManager);
User response = query.select(user)
.from(user)
.where(
user.name.eq("updated").and(user.id.lt(10)) // 인자값의 타입 안전이 보장됨
).fetchOne();
assertEquals(response.getName(), "updated");
}
@Test
@Transactional
void unique_list_failure_nonuniqueresultexception() {
QUser user = QUser.user;
JPAQuery<User> query = new JPAQuery<>(entityManager);
assertThrows(NonUniqueResultException.class, () -> query.select(user)
.from(user)
.where(user.id.lt(10)) // 인자값의 타입 안전이 보장됨
.fetchOne());
}
@Test
@Transactional
void unique_list_failure_no_count() {
QUser user = QUser.user;
JPAQuery<User> query = new JPAQuery<>(entityManager);
User resp = query.select(user)
.from(user)
.where(user.id.gt(10000)) // 인자값의 타입 안전이 보장됨
.fetchOne();
assertNull(resp);
}
fetch()
@Test
@Transactional
void fetch_success() {
QUser user = QUser.user;
JPAQuery<User> query = new JPAQuery<>(entityManager);
List<User> resp = query.select(user)
.from(user)
.where(
user.name.eq("updated").and(user.id.lt(10)) // 인자값의 타입 안전이 보장됨
).fetch();
assertTrue(!resp.isEmpty());
}
@Test
@Transactional
void fetch_fail_empty() {
QUser user = QUser.user;
JPAQuery<User> query = new JPAQuery<>(entityManager);
List<User> resp = query.select(user)
.from(user)
.where(
user.id.gt(100000) // 인자값의 타입 안전이 보장됨
).fetch();
assertTrue(resp.isEmpty());
}
예제) where
더보기
JPAQuery query = new JPAQuery(em);
QItem item = QItem.item;
List<Item> list = query.from(item)
.where(item.name.eq("좋은상품").and(item.price.gt(20000)))
.list(item); //조회할 프로젝션 지정
예제) limit
더보기
QueryModifiers queryModifiers = new QueryModifiers(20L, 10L); //limit, offset;
List<Item> list = query.from(item)
.restrict(queryModifiers)
.list(item);
예제) order by
더보기
QItem item = QItem.item;
List<Item> list = query.from(item)
.where(item.price.gt(20000))
.orderBy(item.price.desc(), item.stockQuantity.asc())
.offset(10).limit(20)
.list(item);
예제) group by
더보기
List<Item> list = query.from(item)
.groupBy(item.price)
.having(item.price.gt(10000))
.list(item);
예제) 조인
더보기
@Test
@Transactional
void join() {
List<User> result = query
.from(user)
.join(user.team, team)
.where(team.name.eq("gsw"))
.fetch();
for (User row : result) {
System.out.println(row);
}
}
예제) 서브쿼리
더보기
@Test
@Transactional
void subquery_max() {
QUser subUser = new QUser("subUser");
List<User> result = query
.from(user)
.where(user.age.eq(new JPAQuery<>()
.select(subUser.age.max())
.from(subUser))
)
.fetch();
for (User row : result) {
System.out.println(row);
}
}
@Test
@Transactional
void subquery_in() {
QTeam subTeam = new QTeam("subTeam");
List<User> result = query
.from(user)
.where(user.team.in(new JPAQuery<>()
.select(subTeam)
.from(subTeam)
.where(subTeam.name.eq("gsw")))
).fetch();
for (User row : result) {
System.out.println(row);
}
}
예제) 프로젝션
더보기
@Test
@Transactional
void projection_single_column() {
List<String> result = userQuery
.select(user.name)
.from(user)
.fetch();
for (String row : result) {
System.out.println(row);
}
}
@Test
@Transactional
void projection_multi_column() {
List<Tuple> result = tupleQuery
.select(user.name, user.age)
.from(user)
.fetch();
for (Tuple row : result) {
System.out.println("[" + row.get(user.age) + "] " + row.get(user.name));
}
}
@Test
@Transactional
void projection_dto() {
List<UserDto> result = userDtoQuery
.select(Projections.constructor(UserDto.class, user.id, user.name))
.from(user)
.fetch();
for (UserDto row : result) {
System.out.println("[" + row.getId() + "] " + row.getName());
}
}
예제) 수정/삭제
더보기
수정
@Test
@Transactional
void update() {
JPAUpdateClause update = new JPAUpdateClause(entityManager, user);
update.set(user.age, user.age.add(1))
.where(user.age.loe(37))
.execute();
}
삭제
@Test
@Transactional
void delete() {
JPADeleteClause delete = new JPADeleteClause(entityManager, user);
long execute = delete.where(user.age.eq(31)).execute();
assertTrue(execute > 0);
}
예제) 메서드 위임
더보기
@Test
@Transactional
void method_delegation() {
List<User> result = userQuery
.from(user)
.where(nameEq("Stephen Curry"), ageGoe(27))
.fetch();
for (User row : result) {
System.out.println("[" + row.getId() + "] " + row.getName());
}
}
private BooleanExpression nameEq(String name) {
return name != null ? user.name.eq(name) : null;
}
private BooleanExpression ageGoe(Integer age) {
return age != null ? user.age.goe(age) : null;
}