Querydsl 문법

이상훈·2022년 11월 1일
0

Jpa

목록 보기
13/16

김영한님의 인프런 강의 '실전! Querydsl'을 참고했습니다.

기본 Q-Type 활용

Q 클래스 인스턴스를 사용하기 위한 2가지 방법이 있다. 같은 테이블을 조인해야 하는 경우가 아니면 기본 인스턴스를 사용하자!!

1. 기본 인스턴스 사용

주로 기본 인스턴스를 static import와 함께 사용한다.

import static study.querydsl.entity.QMember.*;

@Test
public void startQuerydsl3() {
	//member1을 찾아라.
	Member findMember = queryFactory
		.select(member)
		.from(member)
		.where(member.username.eq("member1"))
		.fetchOne();
	
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

실행되는 select 쿼리를 보면 Member의 별칭으로 member1이 나간다. 왜냐하면 Qmember에는 다음과 같은 코드가 있기 때문이다.
public static final QMember member = new QMember("member1");

2. 별칭 직접 지정

@Test
public void startQuerydsl2() {
	//member1을 찾아라.
	QMember m = new QMember("m");

	Member findMember = queryFactory
	    .select(m)
		.from(m)
		.where(m.username.eq("member1"))
		.fetchOne();
       
	assertThat(findMember.getUsername()).isEqualTo("member1");
}
   

실행되는 select 쿼리를 보면 Member의 별칭으로 m이 나간다.


검색 조건 쿼리

Querydsl은 JPQL이 제공하는 모든 검색 조건을 제공한다

member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'

member.username.isNotNull() //이름이 is not null

member.age.in(10, 20) // age in (10,20)
member.age.notIn(10, 20) // age not in (10, 20)
member.age.between(10,30) //between 10, 30

member.age.goe(30) // age >= 30
member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30
member.age.lt(30) // age < 30

member.username.like("member%") //like 검색
member.username.contains("member") // like ‘%member%’ 검색
member.username.startsWith("member") //like ‘member%’ 검색
...

검색 조건은 .and(), .or()를 메서드 체인으로 연결할 수 있다.

@Test
public void search() {
	Member findMember = queryFactory
		.selectFrom(member)
		.where(member.username.eq("member1")
			.and(member.age.eq(10)))
		.fetchOne();
	
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

그중 AND 조건은 파라미터로도 처리할 수 있는데 동적 쿼리를 깔끔하게 만들 수 있어서 이 방법을 더 추천한다.

@Test
public void searchAndParam() {
	List<Member> result1 = queryFactory
		.selectFrom(member)
		.where(member.username.eq("member1"),
			member.age.eq(10))
		.fetch();
	
    assertThat(result1.size()).isEqualTo(1);
}

결과 조회

//List : 리스트 조회, 데이터 없으면 빈 리스트 반환
List<Member> fetch = queryFactory
	.selectFrom(member)
	.fetch();

//단 건 : 결과가 없으면 null, 결과가 둘 이상이면 exception 발생
Member findMember1 = queryFactory
	.selectFrom(member)
	.fetchOne();

//처음 한 건 조회 : limit(1).fetchOne()
Member findMember2 = queryFactory
	.selectFrom(member)
	.fetchFirst();

//페이징에서 사용 : 페이징 정보 포함, total count 쿼리 추가 실행
QueryResults<Member> results = queryFactory
	.selectFrom(member)
	.fetchResults();

//count 쿼리로 변경 : count 쿼리로 변경해서 count 수 조회
long count = queryFactory
	.selectFrom(member)
	.fetchCount();

정렬

// 회원 나이 내림차순, 회원 이름 올림차순, 단 회원 이름이 없으면 마지막에 출력.
List<Member> result = queryFactory
	.selectFrom(member)
	.where(member.age.eq(100))
	.orderBy(member.age.desc(), member.username.asc().nullsLast())
	.fetch();

assertThat(result.size()).isEqualTo(2);

페이징

@Test
public void paging1() {
List<Member> result = queryFactory
	.selectFrom(member)
	.orderBy(member.username.desc())
	.offset(1) //0부터 시작(zero index)
	.limit(2) //최대 2건 조회
	.fetch();

만약 전체 조회 수가 필요하면 .fetchResults()를 사용하자.

QueryResults<Member> queryResults = queryFactory
	.selectFrom(member)
	.orderBy(member.username.desc())
	.offset(1)
	.limit(2)
	.fetchResults();
 
 assertThat(queryResults.getTotal()).isEqualTo(4);
 assertThat(queryResults.getLimit()).isEqualTo(2);
 assertThat(queryResults.getOffset()).isEqualTo(1);
 assertThat(queryResults.getResults().size()).isEqualTo(2);

하지만 실무에서 사용할 때는 주의해야 한다. .fetchResults()는 select 쿼리와 select count 쿼리 2개가 나가는데 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, count 쿼리는 조인이 필요 없는 경우도 있다. 따라서 무작정 .fetchResults를 사용하면 성능이 안 나올 수 있다. 따라서 count 쿼리에 조인이 필요 없는 성능 최적화가 필요하다면 count 전용 쿼리를 별도로 작성해야 한다.


집합

집합 함수

List<Tuple> result = queryFactory
	.select(member.count(),
		member.age.sum(),
		member.age.avg(),
		member.age.max(),
		member.age.min())
	.from(member)
	.fetch();

GroupBy

그룹화된 결과를 제한하려면 having 추가

List<Tuple> result = queryFactory
	.select(team.name, member.age.avg())
	.from(member)
	.join(member.team, team)
	.groupBy(team.name)
    //.having(...)
	.fetch();

조인

기본 조인
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭으로 사용할 Q 타입을 지정하면 된다. join()(==innerJoin), leftJoin(), rightJoin()들을 사용할 수 있다.

/**
 * 팀 A에 소속된 모든 회원
 */
@Test
public void join() throws Exception {
	QMember member = QMember.member;
	QTeam team = QTeam.team;
 
	List<Member> result = queryFactory
		.selectFrom(member)
		.join(member.team, team)
		.where(team.name.eq("teamA"))
		.fetch();
 
	assertThat(result)
		.extracting("username")
		.containsExactly("member1", "member2");
}

세타 조인
세타 조인은 연관관계가 없는 필드로 조인하는 것을 뜻한다. 엔티티 각각을 조인한 후 where 절로 필터링한다. from 절에 조인할 엔티티를 지정해 주면 된다. 하지만 외부 조인은 불가능하다. 만약 외부 조인을 사용하고 싶으면 on 절을 활용해야 한다.

/**
 * 세타 조인(연관관계가 없는 필드로 조인)
 * 회원의 이름이 팀 이름과 같은 회원 조회
 */
@Test
public void theta_join() throws Exception {
	em.persist(new Member("teamA"));
	em.persist(new Member("teamB"));

	List<Member> result = queryFactory
		.select(member)
		.from(member, team)
		.where(member.username.eq(team.name))
		.fetch();
 
	assertThat(result)
		.extracting("username")
		.containsExactly("teamA", "teamB");
}

조인-on절

ON절은 다음과 같은 상황에서 활용한다.

1. 조인 대상 필터링

//예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회

List<Tuple> result = queryFactory
	.select(member, team)
	.from(member)
	.leftJoin(member.team, team).on(team.name.eq("teamA"))
	.fetch();

for (Tuple tuple : result) {
	System.out.println("tuple = " + tuple);
 }
 

참고: on 절을 활용해 조인 대상을 필터링할 때, 외부 조인이 아니라 내부 조인을 사용하면, where 절에서 필터링하는 것과 기능이 동일하다. 따라서 on 절을 활용한 조인 대상 필터링을 사용할 때, 내부 조인 이면 익숙한 where 절로 해결하고, 정말 외부 조인이 필요한 경우에만 이 기능을 사용하자.

2. 연관관계 없는 엔티티 외부 조인

//예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인

em.persist(new Member("teamA"));
em.persist(new Member("teamB"));

List<Tuple> result = queryFactory
	.select(member, team)
	.from(member)
	.leftJoin(team).on(member.username.eq(team.name))
	.fetch();

for (Tuple tuple : result) {
	System.out.println("t=" + tuple);
}

페치 조인

페치 조인으로 Member, Team 한번에 조회.

em.flush();
em.clear();

Member findMember = queryFactory
	.selectFrom(member)
	.join(member.team, team).fetchJoin()
	.where(member.username.eq("member1"))
	.fetchOne();

boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 적용").isTrue();

서브 쿼리

com.querydsl.jpa.JPAExpressions 사용

서브 쿼리 eq 사용

/**
* 나이가 가장 많은 회원 조회
*/
QMember memberSub = new QMember("memberSub");

List<Member> result = queryFactory
	.selectFrom(member)
	.where(member.age.eq(
		JPAExpressions
			.select(memberSub.age.max())
			.from(memberSub)
 	))
	.fetch();

서브 쿼리 goe 사용

/**
* 나이가 평균 나이 이상인 회원
*/
QMember memberSub = new QMember("memberSub");

List<Member> result = queryFactory
	.selectFrom(member)
	.where(member.age.goe(
		JPAExpressions
			.select(memberSub.age.avg())
			.from(memberSub)
	))
	.fetch(); 

서브 쿼리 여러 건 처리 in 사용

/**
* 서브쿼리 여러 건 처리, in 사용
*/
QMember memberSub = new QMember("memberSub");

List<Member> result = queryFactory
	.selectFrom(member)
	.where(member.age.in(
		JPAExpressions
			.select(memberSub.age)
			.from(memberSub)
			.where(memberSub.age.gt(10))
	))
	.fetch(); 

select절에 subquery

List<Tuple> fetch = queryFactory
	.select(member.username,
		JPAExpressions
			.select(memberSub.age.avg())
			.from(memberSub)
	).from(member)
	.fetch();

static import 활용

import static com.querydsl.jpa.JPAExpressions.select;

List<Member> result = queryFactory
	.selectFrom(member)
	.where(member.age.eq(
		select(memberSub.age.max())
			.from(memberSub)
	))
	.fetch();

static import를 적극 활용하자. 가독성이 좋다!!

from 절의 subquery
JPA, JPQL, Querydsl 모두 from 절의 서브 쿼리를 지원하지 않는다. 대신

  • 서브 쿼리를 join으로 변경하자.
    : 가능한 상황도 있고, 불가능한 상황도 있다.
  • 애플리케이션에서 쿼리를 2번 분리해서 실행하자.
    : 성능이 엄청 중요한 시스템이 아닌 이상 이 정도는 괜찮다.
  • nativeSQL을 사용하자.

Case 문

단순한 조건

List<String> result = queryFactory
	.select(member.age
		.when(10).then("열살")
		.when(20).then("스무살")
		.otherwise("기타"))
	.from(member)
	.fetch();

복잡한 조건

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("기타"))
	.from(member)
	.fetch();

Case문은 실제로는 많이 안쓴다. DB에서는 데이터를 가져오는데 집중하고 애플리케이션에서 수정이나 출력 같은 로직들을 구현하는 것이 더 좋다.


상수, 문자 더하기

상수가 필요하면 Expressions.constant(xxx) 사용.

Tuple result = queryFactory
	.select(member.username, Expressions.constant("A"))
	.from(member)
	.fetchFirst();

문자 더하기 concat
stringValue() : 문자가 아닌 다른 타입들을 문자로 변환.

String result = queryFactory
	.select(member.username.concat("_").concat(member.age.stringValue()))
	.from(member)
	.where(member.username.eq("member1"))
	.fetchOne();

출력결과
member1_10


프로젝션 : 대상이 한개

프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다.

List<String> result = queryFactory
	.select(member.username)
	.from(member)
	.fetch();

프로젝션 : 대상이 둘 이상

프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회한다.

1. 튜플 조회

com.querydsl.core.Tuple

List<Tuple> result = queryFactory
	.select(member.username, member.age)
	.from(member)
	.fetch();

for (Tuple tuple : result) {
	String username = tuple.get(member.username);
	Integer age = tuple.get(member.age);
	System.out.println("username=" + username);
	System.out.println("age=" + age);
}

튜플은 Querydsl에 종속적인 타입이다. 따라서 리포지토리 계층 안에서만 사용하고 controller나 service 계층을 경유한다면 DTO를 사용하자!!


2. DTO 조회

@Data
public class MemberDto {
	private String username;
	private int age;
	
    public MemberDto() {
	}
 
 	//@QueryProjection
	public MemberDto(String username, int age) {
		this.username = username;
		this.age = age;
    }
}

1. 순수 JPA 사용

List<MemberDto> result = em.createQuery(
	"select new study.querydsl.dto.MemberDto(m.username, m.age) " +
		"from Member m", MemberDto.class)
	.getResultList();

DTO의 package이름을 다 적어줘야해서 지저분하다.

2. 프로퍼티 접근

List<MemberDto> result = queryFactory
	.select(Projections.bean(MemberDto.class,
		member.username,
		member.age))
	.from(member)
	.fetch();

3. 필드 직접 접근

List<MemberDto> result = queryFactory
	.select(Projections.fields(MemberDto.class,
		member.username,
		member.age))
	.from(member)
	.fetch();

+ 별칭이 다를 때
프로퍼티나, 필드 직접 접근 방식에서는 이름을 통해 구별한다. 만약 이름이 다르다면??

@Data
public class UserDto {
	private String name;
	private int age;
}

ExpressionUtils.as(source,alias) : 필드나, 서브 쿼리에 별칭 적용
username.as("memberName") : 필드에 별칭 적용

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();
   

아래 생성자 사용 방식은 타입을 통해 구별하므로 필드 이름을 맞추지 않아도 된다. 따라서 UserDto를 그대로 사용 가능하다.

4. 생성자 사용

List<MemberDto> result = queryFactory
	.select(Projections.constructor(MemberDto.class,
		member.username,
		member.age))
	.from(member)
	.fetch();
}

5. @QueryProjection

DTO에서 생성자에 @QueryProjection를 추가하고 Querydsl을 compile한후 생성된 Q타입 객체를 사용하는 방법이다.

List<MemberDto> result = queryFactory
	.select(new QMemberDto(member.username, member.age))
	.from(member)
	.fetch();

장점
컴파일러로 타입을 체크할 수 있어서 안전하다.

단점
Querydsl 어노테이션을 유지해야하므로 Querydsl에 의존적이다.


동적 쿼리

동적 쿼리를 해결하는 두 가지 방식이 있다.

1. BooleanBuilder

@Test
public void 동적쿼리_BooleanBuilder() throws Exception {
	String usernameParam = "member1";
	Integer ageParam = 10;

	List<Member> result = searchMember1(usernameParam, ageParam);
	Assertions.assertThat(result.size()).isEqualTo(1);
}

private List<Member> searchMember1(String usernameCond, Integer ageCond) {
	BooleanBuilder builder = new BooleanBuilder();
	if (usernameCond != null) {
		builder.and(member.username.eq(usernameCond));
	}
	if (ageCond != null) {
		builder.and(member.age.eq(ageCond));
	}
	return queryFactory
			.selectFrom(member)
			.where(builder)
			.fetch();
}

2. Where 다중 파라미터 사용

where 다중 파라미터 사용 방식을 사용하자.

장점
1. 재활용성이 높다.
2. 쿼리 자체의 가독성이 높다.
3. 조합 가능하다.

@Test
public void 동적쿼리_WhereParam() throws Exception {
	String usernameParam = "member1";
	Integer ageParam = 10;

	List<Member> result = searchMember2(usernameParam, ageParam);
	Assertions.assertThat(result.size()).isEqualTo(1);
}

private List<Member> searchMember2(String usernameCond, Integer ageCond) {
	return queryFactory
		.selectFrom(member)
		.where(usernameEq(usernameCond), ageEq(ageCond))
		.fetch();
}

private BooleanExpression usernameEq(String usernameCond) {
	return usernameCond != null ? member.username.eq(usernameCond) : null;
}

private BooleanExpression ageEq(Integer ageCond) {
	return ageCond != null ? member.age.eq(ageCond) : null;
}

where 조건에 null값은 무시된다. 따라서 동적 쿼리가 만들어 질 수 있다.

앞서 장점으로 조합 가능하다라고 언급하였는데 아래 코드를 참고하자,

private BooleanExpression allEq(String usernameCond, Integer ageCond) {
	return usernameEq(usernameCond).and(ageEq(ageCond));
}

수정, 삭제 벌크 연산

쿼리 한번으로 대량 데이터 수정

long count = queryFactory
	.update(member)
	.set(member.username, "비회원")
	.where(member.age.lt(28))
	.execute();

기존 숫자에 1 더하기

long count = queryFactory
	.update(member)
	.set(member.age, member.age.add(1))
	.execute();

쿼리 한번으로 대량 데이터 삭제

long count = queryFactory
	.delete(member)
	.where(member.age.gt(18))
	.execute();

주의 사항 : JPQL 배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 쿼리를 실행하고 반드시 영속성 컨텍스트를 초기화하자.


SQL function 호출하기

SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있다.

//member M으로 변경하는 replace 함수 사용
String result = queryFactory
	.select(Expressions.stringTemplate("function('replace', {0}, {1}, {2})	", member.username, "member", "M"))	
	.from(member)
	.fetchFirst();
    
//소문자로 변경해서 비교.
String result = queryFactory
	.select(member.username)
	.from(member)
	.where(member.username.eq(Expressions.stringTemplate("function('lower', {0})", member.username)))

lower 같은 ansi 표준 함수들은 querydsl이 상당 부분 내장하고 있다. 따라서 다음과 같이 처리해도 결과는 같다.

.where(member.username.eq(member.username.lower()))

기타

EntityManager의 em.find() : 먼저 영속성 컨텍스트 조회 후 없으면 DB에 쿼리 나감.
JPQL, Querydsl : 무조건 select 쿼리가 DB에 나감.

profile
Problem Solving과 기술적 의사결정을 중요시합니다.

1개의 댓글

comment-user-thumbnail
2022년 11월 6일

querydsl 참고할 문서가 거의 없는데 잘 정리되어 있네요 .... "@QueryProjection" 관련 적용 방법을 못 찾았었는데 .. 감사합니다. 나만 알고 싶은 querydsl ... 쿼리 관련 개발 소요 시간이 거의 없어짐 ~ ㅋ 완성 단계까지 가기가 멀고 험난하지만 ...

답글 달기