QueryDSL - 문법 정리

박민수·2023년 11월 14일
0

JPA

목록 보기
7/24
post-thumbnail

QueryDSL 문법 정리

해당 포스팅에서는 QueryDSL 문법에 대해 정리해 보고자 한다.

프로젝션과 결과 반환

기본

프로젝션이란 select 대상을 지정하는 것 이다.

  • 프로젝션 대상이 하나면 타입을 명확하게 지정 할 수 있다.
  • 프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회한다.
// 프로젝션 대상이 하나일 때
List<String> result = queryFactory
    .select(member.username)
    .from(member)
    .fetch();
// 프로젝션 대상이 둘 이상일 때 (튜플 조회)
List<Tuple> r esult = 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);
}

DTO 조회 (순수 JPA)

순수 JPA에서 DTO 타입으로 조회하려면 생성자 방식만 지원하기 때문에 new 명령어를 사용해서 DTO의 package 경로를 일일이 다 적어줘야 한다.

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

DTO 조회 (QueryDsl)

QueryDsl 빈 생성을 통해 결과를 DTO로 반환할 수 있다. 다음 3가지 방법을 지원한다.

  1. 프로퍼티 접근
  2. 필드 직접 접근
  3. 생성자 사용

프로퍼티 접근 - Setter

queryFactory
	.select(Projections.bean(MemberDto.class, 
    	member.username, 
        member.age))
    .from(member)
    .fetch();

필드 직접 접근

queryFactory
	.select(Projections.fields(MemberDto.class, 
    	member.username, 
        member.age))
    .from(member)
    .fetch();

별칭이 다를 때

프로퍼티나, 필드 접근 생성 방식에서 이름이 다를 때 해결 방안

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

생성자 사용

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

@QueryProjection

생성자에 @QueryProjection 어노테이션을 적용하면 MemberDto용 Q파일이 생성된다. 이 방법은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법이다. 다만 DTO에 QueryDsl 어노테이션을 유지해야 하는 점과 DTO까지 Q 파일을 생성해야 하는 단점이 있다

@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;
    }
}
List<MemberDto> result = queryFactory
    .select(new QMemberDto(member.username, member.age))
    .from(member)
    .fetch();

distinct

distinct는 JPQL의 distinct와 같다.

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

동적 쿼리

QuaryDsl 에서 동적 쿼리를 해결하기 위한 방법으로는 2가지가 있다.

  1. BooleanBuilder 사용
  2. Where 다중 파라미터 사용

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

Where 다중 파라미터 사용

where 다중 파라미터를 사용하면 메서드를 다른 쿼리에서도 재활용 할 수 있고, 쿼리 자체의 가독성이 높아진다. 참고로 where 절에 null이 오면 해당 조건은 무시된다.

@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;
}

+) 조건

조합 가능
BooleanExpression을 통해 단일 조건 메소드들을 합쳐서 하나의 새로운 조건 메소드로 뽑을 수 있다.

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

벌크 연산

벌크 연산이란 대량의 데이터를 한 번에 수정하거나 삭제하는 방법이다. 참고로 QueryDsl에서의 모든 벌크 연산은 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에, 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 안전하다.

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

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

기존 숫자에 1 더하기

  • multiply(x) : 곱하기
long count = queryFactory
    .update(member)
    .set(member.age, member.age.add(1))
    .execute();

update member
set age = age + 1

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

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

SQL function

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

member를 M으로 변경하는 replace 함수 사용

List<String> result = queryFactory
    .select(Expressions.stringTemplate(
    	"function('replace', {0}, {1}, {2})", 
    	member.username, "member", "M"))
    .from(member)
    .fetchFirst();

소문자로 변경해서 비교

List<String> result = queryFactory
    .select(member.username)
    .from(member)
    .where(member.username.eq(
        Expressions.stringTemplate("function('lower', {0})", member.username)))
    .fetch();

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

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

참조
https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84/dashboard

profile
안녕하세요 백엔드 개발자입니다.

0개의 댓글