티스토리 뷰

아래 내용은 QueryDSL - 김영한 강의 영상을 보고 실무에서 필요할 때

찾아보려고 정리한 내용이며 QueryDSL 문법에 순서를 적용하여 찾아보기 쉽게 작성했습니다.

그리고 아래 문법을 테스트할 때 사용했던 모든 버전은 QueryDSL 설정 글에서 확인하시면 됩니다

 

아래로 조금만 내리시면 작성된 문법들에 대한 표가 있고

필요한 문법 이름을 복사한 후 문자열 찾기 하시면 해당 내용으로 빠르게 가실 수 있습니다

 

사전 준비

Q엔티티 static import, JPAQueryFactory 생성

import static com.jpa.jpaboilerplate.entity.QMember.member;
import static com.jpa.jpaboilerplate.entity.QTeam.team;

JPAQueryFactory queryFactory = new JPAQueryFactory(em);

 

테스트용 Member, Team 엔티티 설정

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// 연관 관계 설정된 필드 넣지 말 것
@ToString(of = {"id", "username", "age"})
public class Member extends BaseEntity {

    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    public Member(String username) {
        this(username, 0);
    }

    public Member(String username, int age) {
        this(username, age, null);
    }

    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }

    //== 연관 관계 편의 메서드 ==//
    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {
    @Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}

 

1. 기본 select, from, selectFrom
2. 별칭 as
3. 비교 eq (==), eq.not (!=)
4. 조건절 and, or
5. 결과 fetch, fetchOne, fetchFirst and Deprecated methods
6. Deprecated fetchCount 해결 방법
7. 정렬 desc, asc
8. Tuple (QueryDSL 반환 타입)
9. 집계 count, sum, avg, max, min
10. groupBy, having
11. 조인, 세타 조인(막무가내 조인)
12. 조인 on 절
13. join fetch
14. sub query
15. case, CaseBuilder(case 내용이 복잡한 경우)
16. 데이터에 상수 추가
17. concat (문자열 더하기)
18. 프로젝션(select 반환 대상), 대상 1~2개, DTO 반환, @QueryProjection
19. 동적 쿼리
20. 수정, 삭제 벌크
21. SQL Function

 

1. 기본 select, from, selectFrom

@Test
void Querydsl() {
    Member findMember = queryFactory
            .select(member)
            .from(member)
            // 조회 대상이 엔티티 하나인 경우 .selectFrom() 사용 가능
            //.selectFrom(member)
            .fetchOne();
}

 

2. 별칭 as

만약 같은 엔티티에 대해서 조인을 진행해야 될 경우

QMember subMember = new QMember("subMember");

와 같이 새로운 별칭을 가진 멤버를 생성하고 조인해야됩니다

@Test
void as() {
    List<String> memberNames = queryFactory
            .select(member.username.as("memberName"))
            .from(member)
            .fetch();
}

 

3. 비교 eq (==), eq.not (!=)

@Test
void eqAndEqNot() {
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq("usernameA"))
            .fetchOne();

    List<Member> findUsers = queryFactory
            .selectFrom(member)
            .where(member.username.eq("usernameA").not())
            .fetch();
}

 

4. 조건절 and, or

콤마(,) 를 사용한 방법은 and 조건과 같습니다

@Test
void and_or() {
    List<Member> findMembers = queryFactory
            .selectFrom(member)
            .where(
                    member.username.eq("usernameA"),
                    member.age.eq(20)
            )
            //.where(member.username.eq("usernameA").and(member.age.eq(20)))
            //.where(member.username.eq("usernameA").or(member.age.eq(20)))
            .fetch();
}

 

5. 결과 fetch, fetchOne, fetchFirst and Deprecated methods

  • Deprecated methods
    • GroupBy, Having절을 사용할 경우 이슈가 많다고 합니다
    • fetchCount: 토탈 카운트
    • fetchResult: 데이터 + 토탈 카운트
@Test
void result() {
    queryFactory
            .selectFrom(member)
            .where(member.age.goe(10)) // goe 설명은 따로 있습니다
            .fetch(); // List Data
            //.fetchOne(); // Single Data
            //.fetchCount(); // deprecated
            //.fetchResults(); // deprecated
            //.fetchFirst(); // limit(1) + fetchOne() 과 같습니다
}

 

6. Deprecated fetchCount 해결 방법

@Test
void totalCount() {
    Long totalCount = queryFactory
            .select(member.count())
            .from(member)
            .fetchOne();
}

 

7. 정렬 desc, asc

아래에 적용한 정렬 조건은 아래와 같습니다

회원 나이 내림 차순, 회원 이름 올림 차순, 회원 이름이 없으면 마지막에 출력

@Test
void sort() {
    List<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.age.desc(), member.username.asc().nullsLast())
            .fetch();
}

 

8. Tuple (QueryDSL 반환 타입)

Tuple 클래스는 여러 엔티티, 값 또는 여러 타입들이 포함될 경우 반환하기 위해 사용합니다

아래는 List<Tuple> 의 필드를 사용하는 방법입니

@Test
void tuple() {
    List<Tuple> tuples = queryFactory
            .select(member.username, team.name)
            .from(member)
            .join(member.team, team)
            .fetch();

    for (Tuple tuple : tuples) {
        String username = tuple.get(member.username);
        String teamName = tuple.get(team.name);
    }
}

 

9. 집계 count, sum, avg, max, min

@Test
void aggregation() {
    Tuple tuple = queryFactory
            .select(
                    member.count(),
                    member.age.sum(),
                    member.age.avg(),
                    member.age.max(),
                    member.age.min())
            .from(member)
            .fetchOne();

    Long count = tuple.get(member.count());
    Integer num = tuple.get(member.age.sum());
    Double avg = tuple.get(member.age.avg());
    Integer max = tuple.get(member.age.max());
    Integer min = tuple.get(member.age.min());
}

 

10. groupBy, having

@Test
void groupAndHaving() {
    List<Tuple> tuples = queryFactory
          .select(team.name, member.age.avg())
          .from(member)
          .join(member.team, team)
          .groupBy(team.name).having(member.age.avg().gt(10)) // 나이 평균이 10보다 큼
          .fetch();

    for (Tuple tuple : tuples) {
       String teamName = tuple.get(team.name);
       Double ageAvg = tuple.get(member.age.avg());
    }
}

 

11. 조인, 세타 조인

세타 조인은 막무가내로 조인하는 느낌이 강하며 아래 예시는

사용자 이름과 팀 이름이 같은 경우 조인을 하는 예시입니다

@Test
void join() {
    // 일반 조인
    List<Member> members = queryFactory
         .selectFrom(member)
         .join(member.team, team) // .join() == .innerJoin()
         //.leftJoin()
         //.rightJoin()
         .fetch();
}
@Test
void thetaJoin() {        
    // 세타 조인
    List<Member> result = queryFactory
    	.select(member)
        .from(member, team)
        .where(member.username.eq(team.name))
        .fetch();
}

 

12. 조인 on 절

@Test
void on() throws Exception {
    List<Tuple> tuples = queryFactory
          .select(member, team)
          .from(member)
          .leftJoin(team).on(member.username.eq(team.name))
          .where(team.name.eq("teamA"))
          .fetch();
}

 

13. join fetch

우선 member에서 team은 FetchType.LAZY 로 설정되어 있습니다

EntityManagerFactory 를 호출하는 이유는 .fetchJoin() 을 적용했을 때 Member, Team 을 조인해서

데이터를 쿼리 한 번에 가져오는지 그리고 영속성 컨텍스트에서 관리중인지 확인하기 위함입니다

@PersistenceUnit
EntityManagerFactory emf;

@Test
void join() {
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq("usernameA"))
            .fetchOne();

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).isFalse();
}

@Test
void fetchJoin() throws Exception {
    Member findMember = queryFactory
            .selectFrom(member)
            .join(member.team, team).fetchJoin()
            .where(member.username.eq("usernameA"))
            .fetchOne();

	boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).isTrue();
}

 

14. sub query

공부한 시점에 from 절에 서브쿼리는 JPA, QueryDSL 둘 다 지원하지 않았습니다

혹시나 가능하도록 업데이트 되었다면 해당 정보를 댓글로 남겨주시면 감사하겠습니다

그리고 지원하지 않은 시점에서 from 절에 sub query 가 필요할 정도로 복잡해진다면

nativeSQL 사용하여 해결할 수 있으니 검색해보시면 좋을 것 같습니

@Test
void subQuery() {
    //when
    QMember memberSub = new QMember("memberSub");
    List<Member> findMembers = queryFactory
            .selectFrom(member)
            .where(member.age.eq(
                    JPAExpressions
                            .select(memberSub.age.max()) // max age
                            .from(memberSub)
            ))
            .fetch();

    //then
    assertThat(findMembers)
            .extracting("age")
            .containsExactly(40);
}

 

15. case, CaseBuilder(case 내용이 복잡한 경우)

case 절을 성능에 대한 관점으로 바라볼 때 쿼리에서 적용하여 분류하는 것 보다

어플리케이션 레벨에서 처리하는게 더 좋다고 합니다 :)

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

@Test
void complexCase() throws Exception {
    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();
}

 

16. 데이터에 상수 추가

아래 로직을 보시면 상수를 포함한 조회를 진행하는 것 처럼 보이나

실제 전달한 쿼리 로그를 확인해 보시면 상수에 대한 내용은 없습니다

데이터를 받아온 후 상수 값을 넣는 방식을 사용합니다

@Test
void constant() {
    List<Tuple> result = queryFactory
            .select(member, Expressions.constant("A"))
            .from(member)
            .fetch();
}

 

17. concat (문자열 더하기)

@Test
void concat() {
    //when
    List<String> result = queryFactory
            .select(member.username.concat("_").concat(member.age.stringValue()))
            .from(member)
            .fetch();
}

 

18. 프로젝션(select 반환 대상), 대상 1~2개, DTO 반환, @QueryProjection

 

18_1. 반환 대상이 1~2개

18_2. 프로젝션을 DTO 로 반환하는 세 가지 방법

18_3. 프로젝션을 DTO 로 반환의 단점 극복, @QueryProjection 사용

 

18_1. 반환 대상이 1~2개

@Test
void projection() {
    // 반환 대상 1개(username)
    List<String> targetOne = queryFactory
            .select(member.username).from(member).fetch();

    // 반환 대상 2개(username, age)
    List<Tuple> targetTwo = queryFactory
            .select(member.username, member.age).from(member).fetch();
}

 

18_2. 프로젝션을 DTO 로 반환하는 세 가지 방법

Projections.bean, Projections.fields, Projections.constructor

Projections.constuctor 를 제외한 나머지는 필드명이 다를 경우 null 값이 주입됩니다

그리고 필드명이 다를 때 별칭을 아래와 같이 적용하여 Dto 와 맞춰주면 문제를 해결할 수 있습니다

아래 방법들의 큰 단점은 안에 잘못된 필드를 마음대로 추가해도 컴파일 레벨에서 확인하지 못합니다

해당 문제는 런타임 레벨에서 확인되며 즉 사용자에 의해 발견됩니다

@Test
void projectionToDto() {
    List<UserDto> users = queryFactory
            //.select(Projections.bean(UserDto.class, member.username, member.age)) // 필드명이 달라서 이름이 null
            //.select(Projections.bean(UserDto.class, member.username.as("name"), member.age)) 필드명 다를 때 as 사용하는 방법
            //.select(Projections.fields(UserDto.class, member.username, member.age)) // fields 또한 필드명이 달라서 이름이 null
            //.select(Projections.fields(UserDto.class, member.username.as("name"), member.age))
            //.select(Projections.constructor(UserDto.class, member.username, member.age)) // 타입만 같으면 결과가 정상적으로 출력
            .select(Projections.fields(UserDto.class, // 서브 쿼리의 결과에 별칭 사용하는 방법
                    member.username.as("name"),
                    Expressions.as(JPAExpressions
                            .select(member.age.max())
                            .from(member), "age")
            ))
            .from(member)
            .fetch();
}

 

18_3. 프로젝션을 DTO 로 반환의 단점 극복, @QueryProjection 사용

@QueryProjection 적용 후 Maven, Gradle 에서 (Gradle 기준)Tasks - compileJava 실행하여

Q 로 시작하는 Dto 가 생성되어야 사용 가능합니다

아래와 같이 적용하면 생성자에 잘못된 필드가 들어갈 때 즉 컴파일 단계에서 에러를 잡을 수 있습니다

import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class MemberDto {
    private String username;
    private int age;

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

@Test
void queryProjectionAnnotation() {
    List<MemberDto> result = queryFactory
            .select(new QMemberDto(member.username, member.age))
            .from(member)
            .fetch();
}

 

19. 동적 쿼리

동적 쿼리를 적용하는 방법은 아래와 같이 심플하게 하나의 메서드에서 모든 조건을 만들고

처리하는 방법과 조건이 될 수 있는 여러사항을 모두 구분하여 필요한 조합으로 묶어

적용하는 2가지 방법이 있습니다

 

19_1. 조건을 하나의 메서드에 직접 명시해서 해결하는 방법

19_2. 조건을 BooleanExpression 로 분리하여 묶어서 해결하는 방법(실무에서 사용 권장)

 

19_1. 조건을 하나의 메서드에 직접 명시해서 해결하는 방법

@Test
void booleanBuilder() {
    String username = "usernameA";
    int age = 10;
    List<Member> members = searchMembers(username, age);
    for (Member m : members) {
        System.out.println("m = " + m);
    }
}

private List<Member> searchMembers(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();
}

 

19_2. 조건을 BooleanExpression 로 분리하여 묶어서 해결하는 방법(실무에서 사용 권장)

@Test
void dynamicQuery() {
    String username = "usernameA";
    int age = 10;

    List<Member> members = queryFactory
            .selectFrom(member)
            // predicate 사용하여 각 각의 메서드 표현
            //.where(usernameEq(username), ageEq(age))
            // BooleanExpression 사용하여 조합해서 한 번에 표현
            .where(AllEq(username, age))
            .fetch();

    for (Member member : members) {
        System.out.println("member = " + member);
    }
}

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

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

// 메서드를 조합해서 사용하는 방법
private BooleanExpression AllEq(String username, Integer age) {
    return usernameEq2(username).and(ageEq2(age));
}

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

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

 

20. 수정, 삭제 벌크

벌크 연산은 영속성 컨텍스트에 영향을 주지 않습니다
벌크 연산 이 후 비즈니스 로직이 필요한 경우는 영속성 컨텍스트를 모두 비우고

필요한 엔티티를 다시 조회한 후 비즈니스 로직을 적용해야 됩니다
벌크 연산 이 후 비즈니스 로직을 가지지 않은 방법을 권장합니다

@Test
void bulk() {
    long 수정_영향_받은_행_수 = queryFactory
            .update(member)
            //.set(member.username, "비회원") // 문자열 변경
            .set(member.age, member.age.add(1)) // 숫자, -숫자 변경
            .where(member.age.lt(28))
            .execute();

    long 삭제_영향_받은_행_수 = queryFactory
            .delete(member)
            .where(member.age.gt(18))
            .execute();

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

 

21. SQL Function

DB에서 제공하는 함수를 사용하는 방법

ANSI SQL Function(모든 DB에서 기본적으로 제공하는 function)은 QueryDSL에서도 지원

추가적인 기능이 필요한 경우 'DB명' + Dilect 상속 받고 커스텀 해야합니다

@Test
void sqlFunction() {
    List<String> result = queryFactory
            .select(Expressions.stringTemplate(
                    "function('lower', {0})", member.username)) // where 절에서도 사용 가능
			//.select(member.username.lower()) // QueryDSL 에서 지원하는 함수
            .from(member)
            .fetch();
}

'JPA > QueryDSL' 카테고리의 다른 글

QueryDSL 설정, QueryRepositorySupport & Paging, WEB 지원  (0) 2023.11.13