build.gradle
dependencies {
...
// QueryDSL JPA 라이브러리
implementation 'com.querydsl:querydsl-jpa'
// QueryDSL 관련된 쿼리 타입(QClass)을 생성할 때 필요한 라이브러리로, annotationProcessor을 사용하여 추가
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
// java.lang.NoClassDefFoundError(javax.annotation.Entity) 발생 시 추가
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
// java.lang.NoClassDefFoundError(javax.annotation.Generated) 발생 시 추가
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
}
test {
useJUnitPlatform()
}
// 빌드시 QClass 소스도 함께 빌드하기 위해서 sourceSets에 해당 위치를 추가해준다.
def querydslSrcDir = 'src/main/generated'
sourceSets {
main.java.srcDir querydslSrcDir
}
// annotaion processors 에서 생성한 소스 파일을 저장할 디렉토리를 지정해 준다.
tasks.withType(JavaCompile) {
options.generatedSourceOutputDirectory = file(querydslSrcDir)
}
// clean 시 쿼리 타입 삭제
clean {
delete file(querydslSrcDir)
}
annotationProcessor는 자바 컴파일러 플러그인 일종으로, 컴파일 단계에서 프로젝트 내의 @Entity 애노테이션을 선언한 클래스를 탐색하여 com.querydsl.apt.jpa.JPAAnnotationProcessor를 통해 쿼리 타입(QClass)을 생성한다.
생성된 쿼리 타입은 com.querydsl.core.QueryFactory에 주입하여 사용한다.
기본 사용법
QMember m = new QMember("m");
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1"))
.fetchOne();
기본 Q 생성
쿼리 타입은 사용하기 편리하도록 기본 인스턴스를 보관하고 있다. 하지만 같은 엔티티를 조인하거나 서브쿼리에 사용하면 같은 별칭이 사용되므로 이때는 별칭을 직접 지정해서 사용해야 한다.
서브 쿼리를 사용할 때 외에는 기본 인스턴스르 static import와 함께 사용하는 것을 권장한다.
별칭 지정
QMember qMember = new QMember("m");
기본 인스턴스 사용
QMember qMember = QMember.member;
검색 조건 쿼리
검색 조건은 메서드 체인으로 연결할 수 있고 모든 검색 조건을 제공하고 있다.
함수 | SQL |
eq(1) | == 1 |
ne(1) | != 1 |
eq(1).not() | != 1 |
isNotNull() | is not null |
in(10,20) | in (10,20) |
notIn(10,20) | not in (10,20) |
between(10,30) | between 10, 30 |
goe(30) | >= 30 |
ge(30) | > 30 |
loe(30) | <= 30 |
lt(30) | < 30 |
like("member") | like 'member' |
contains("member") | like '%member%' |
startWith("member") | like 'member%' |
결과 조회 함수
함수 | 설명 |
fetch() | 리스트 조회, 데이터 없으면 빈 리스트 |
fetchOne() | 단건 조회, 결과 없으면 null, 둘 이상이면 com.querydsl.coreNonUniqueResultException |
fetchFirst() | limit(1).fetchOne() |
fetchResults() | Deprecated |
fetchCount() | Deprecated |
정렬과 페이징
전체 데이터 수를 함께 조회하는 함수는 Deprecated 되었다.
count 전용 쿼리를 별도로 작성해야 한다.
함수 | 설명 |
desc(), asc() | 일반 정렬 |
nullsLast(), nullsFirst() | null 데이터 순서 부여 |
offset().limit() | 시작 위치, 개수 |
restrict(QueryModifiers(limit,offset)) | QueryModifiers 객체 사용 |
GroupBy, Having
queryFactory.from(member).groupBy(member.age).having(member.age.gt(20)).fetch();
조인
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭으로 사용할 Q타입을 지정한다.
기본 조인
QMember member = QMember.member;
QTeam team = QTeam.team;
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
List<Member> result = queryFactory.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("TeamA"))
.fetch();
세타 조인
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
List<Member> result = queryFactory
.select(member)
.from(member,team) // from 절에 여러 엔티티를 선택해서 세타 조인한다.
.where(member.username.eq(team.name))
.fetch();
// SQL
select
member0_.member_id as member_i1_0,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_ cross
join
team team1_
where
member0_.username=team1_.name
조인 on 절
일반 조인 : leftJoin(member.team, team)
on 조인 : from(member).leftJoin(team).on(조건식)
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(member.team, team).on(team.name.eq("teamA"))
.fetch();
// SQL
select
member0_.member_id as member_i1_0_0_,
team1_.team_id as team_id1_1_1_,
member0_.age as age2_0_0_,
member0_.team_id as team_id4_0_0_,
member0_.username as username3_0_0_,
team1_.name as name2_1_1_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
and (
team1_.name=?
)
// 연관관계 없는 외부 조인
List<Tuple> result = queryFactory
.select(member,team)
.from(member)
.leftJoin(team).on(member.username.eq(team.name))
.fetch();
// SQL
select
member0_.member_id as member_i1_0_0_,
team1_.team_id as team_id1_1_1_,
member0_.age as age2_0_0_,
member0_.team_id as team_id4_0_0_,
member0_.username as username3_0_0_,
team1_.name as name2_1_1_
from
member member0_
left outer join
team team1_
on (
member0_.username=team1_.name
)
페치 조인
즉시로딩으로 한번에 조인한다. 조인 뒤에 fetchJoin()이라고 추가하면 된다.
Member findMember = queryFactory
.selectFrom(member)
.join(member.team,team).fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
// SQL
select
member0_.member_id as member_i1_0_0_,
team1_.team_id as team_id1_1_1_,
member0_.age as age2_0_0_,
member0_.team_id as team_id4_0_0_,
member0_.username as username3_0_0_,
team1_.name as name2_1_1_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.username=?
서브 쿼리
com.querydsl.jpa.JPAExpressions 객체를 이용한다.
FROM 절의 서브쿼리는 지원하지 않는다.
QMember memberSub = new QMember("memberSub"); // 별칭
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub)
)).fetch();
Case 문
void basicCase() {
List<String> result = queryFactory
.select(member.age
.when(10).then("10")
.when(20).then("20")
.otherwise("00")
).from(member)
.fetch();
}
// SQL
select
case
when member0_.age=? then ?
when member0_.age=? then ?
else '00'
end as col_0_0_
from
member member0_
void complexCase() {
List<String> result = queryFactory
.select(
new CaseBuilder()
.when(member.age.between(0, 20)).then("0~20")
.when(member.age.between(21, 30)).then("21~30")
.otherwise("00")
).from(member)
.fetch();
}
// SQL
select
case
when member0_.age between ? and ? then ?
when member0_.age between ? and ? then ?
else '00'
end as col_0_0_
from
member member0_
상수 문자 더하기
void constant() {
Member member1 = new Member();
member1.setUsername("kyun");
em.persist(member1);
List<Tuple> result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
for (Tuple tuple : result) {
System.err.println(tuple);
}
}
// 결과 [kyun, A]
void concat() {
Member member1 = new Member();
member1.setUsername("kyun");
member1.setAge(100);
em.persist(member1);
List<String> result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("kyun"))
.fetch();
}
// 결과 s = kyun_100
DTO 결과 반환
// bean()
// getter, setter, NoArgConstructor 필요
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
// fields()
// 바로 주입, getter, setter 필요 없음
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
// constructor()
// 생성자 필요
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
// DTO 필드명이 다를 때
List<UserDto> fetch = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
ExpressionUtils.as(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")
))
.from(member)
.fetch();
@QueryProjection
DTO 생성자에 붙여주면 DTO도 Q파일로 생성된다.
컴파일 시점에 오류를 잡아낼 수 있는 장점이 있지만 DTO가 QueryDSL에 대한 의존성이 생긴다는 단점이 있다.
import com.querydsl.core.annotations.QueryProjection;
public class MemberDto {
private String username;
private int age;
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
수정, 삭제 배치 쿼리(벌크 연산)
JPQL 배치 쿼리와 같이 영속성 컨텍스트를 무시하고 데이터베이스를 직접 쿼리한다.
따라서 배치 쿼리 이후 같은 트랜잭션에서 다시 조회할 경우에는 em.flush()와 em.clear()를 해줘야 한다.
public void bulkUpdate() {
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute();
}
동적 쿼리
BooleanBuilder
private List<Member> searchMember(String nameCond, Integer ageCond) {
BooleanBuilder builder = new BooleanBuilder();
if(nameCond != null) {
builder.and(member.name.eq(nameCond));
}
if(ageCond != null) {
builder.and(member.age.eq(ageCond));
return queryFactory
.selectFrom(member)
.where(builder)
.fetch();
}
BooleanExpression
private List<Member> searchMember(String nameCond, Integer ageCond) {
return queryFactory
.selectFrom(member)
.where(nameEq(nameCond), ageEq(ageCond))
.fetch();
}
private BooleanExpression nameEq(String nameCond) {
if (nameCond == null) {
return null;
}
return member.name.eq(nameCond);
}
private BooleanExpression ageEq(Integer ageCond) {
if (ageCond == null) {
return null;
}
return member.age.eq(ageCond);
}
함께 사용
// query
private List<Post> search(final Map<String, String> searchCondition) {
return queryFactory
.selectfrom(post)
.where(allCond(searchCondition))
.fetch();
}
// BooleanBuilder
private BooleanBuilder allCond(final Map<String, String> searchCondition) {
BooleanBuilder builder = new BooleanBuilder();
return builder
.and(titleLike(searchCondition.getOrDefault(TITLE.getParamKey(), null)))
.and(categoryEq(searchCondition.getOrDefault(CATEGORY.getParamKey(), null)))
.and(tagLike(searchCondition.getOrDefault(TAG.getParamKey(), null)))
.and(stateEq(searchCondition.getOrDefault(STATE_CODE.getParamKey(), null)))
.and(withInDays(searchCondition.getOrDefault(DAYS_AGO.getParamKey(), null)));
}
// 조건1
private BooleanExpression titleLike(final String title) {
return StringUtils.hasText(title) ? post.title.contains(title) : null;
}
// 조건2
private BooleanExpression categoryEq(final String category) {
if (!StringUtils.hasText(category)) return null;
Category instance = Category.getInstance(category);
return post.category.eq(instance);
}
// 조건3
private BooleanExpression titleLike(final String tag) {
return StringUtils.hasText(tag) ? post.tag.contains(tag) : null;
}
// 조건4
private BooleanExpression stateEq(final String stateCode) {
if (!StringUtils.hasText(stateCode)) return null;
Integer code = convertInteger(stateCode);
RecruitStatus instance = RecruitStatus.getInstance(code);
return post.status.eq(instance);
}
// 조건5
private BooleanExpression withInDays(final String daysAgo) {
if (!StringUtils.hasText(daysAgo)) return null;
Long days = convertLong(daysAgo);
LocalDate now = LocalDate.now();
LocalDate startDate = now.minusDays(days);
LocalDateTime startDateTime = LocalDateTime.of(startDate, LocalTime.MIN);
return post.createdDate.goe(startDateTime);
}
메소드 위임
쿼리 타입에 검색 조건을 직접 정의할 수 있다. WHERE 절이 깔끔해진다.
QClass를 컴파일해야 한다.
@QueryEntity
public class MemberExpression {
@QueryDelegate(Member.class)
public static BooleanExpression isOlder(QMember member, Integer age) {
return member.age.gt(age);
}
}
// QMember
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QMember extends EntityPathBase<Member> {
...
public BooleanExpression isOlder(Integer age) {
return MemberExpression.isOlder(this, age);
}
...
}
// 사용
List<Member> fetch = queryFactory.selectFrom(member)
.where(member.isOlder(10))
.fetch();
'JVM > JPA' 카테고리의 다른 글
스토어드 프로시저(JPA 2.1) (0) | 2023.06.30 |
---|---|
네이티브 SQL (0) | 2023.06.30 |
JPQL 다형성 쿼리, 사용자 정의 함수, Named 쿼리 (0) | 2023.06.28 |
JPQL 조건식 (0) | 2023.06.28 |
JPQL 서브쿼리 (0) | 2023.06.26 |