QueryDSL에 대해 - 기본

쿠우·2023년 1월 10일
0

Querydsl

장점

  • 동적 쿼리에 대해서 해결가능
  • java로 sql문을 작성하면서 컴파일러로 오류를 잡아 낼 수 있다.
    (자동 완성 기능까지 사용 할 수 있다.)
  • 메서드로 따로 뽑고 재사용 가능하다.

검증용 Q타입

  • gradle에 querydsl 관련한 설정들을 추가해주고 entity를 만들면 Q타입이 생성된다.
  • querydsl 에서는 Q타입에 의해서 코드를 관리한다.

Q 타입 생성 확인

  • 디렉토리 경로 build - generated - querydsl 들어가면 Q~~~.java 파일이 생성되어있다.
  • Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다.
    (build는 ignore 처리하기 때문에 따로 안해도 될 듯)

JPQL 과 Querydsl 비교

@Test
public void startJPQL() {
    //member1을 찾아라.
    String qlString =
            "select m from Member m " +
                    "where m.username = :username";
    Member findMember = em.createQuery(qlString, Member.class)
            .setParameter("username", "member1")
            .getSingleResult();
    assertThat(findMember.getUsername()).isEqualTo("member1");
}
@Test
public void startQuerydsl() {
    //member1을 찾아라.
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QMember m = new QMember("m");
    Member findMember = queryFactory
            .select(m)
            .from(m)
            .where(m.username.eq("member1"))//파라미터 바인딩 처리
            .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo("member1");
}
  • EntityManager 로 JPAQueryFactory 생성
  • JPQL: 파라미터 바인딩 직접, Querydsl: 파라미터 바인딩 자동 처리
  • Querydsl은 JPQL 빌더
  • JPQL: 문자(실행 시점 오류), Querydsl: 코드(컴파일 시점 오류)

Tip. JPAQueryFactory를 필드로 제공하면 동시성 문제는 어떻게 될까? 동시성 문제는 JPAQueryFactory를 생성할 때 제공하는 EntityManager(em)에 달려있다. 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다.

기본 Q-Type 활용

  • 2가지 방법이 있다
  • 방법1 : QMember qMember = new QMember("m"); //별칭 직접 지정
    (같은 테이블을 조인해야하는 경우에만 이런식으로 선언해서 사용한다.)
  • 방법2 : Q-Type 내부에 이미 static으로 설정되어있는 기본 인스턴스에 대해 static import 하여 사용한다.

검색조건 쿼리

  • where() 검색 조건은 .and() , . or() 를 메서드 체인으로 연결할 수 있다.
    (and는 쉼표(,)로 대신 할 수 있다. 선생님은 쉼표(,) 방식을 선호)

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%’ 검색
...

-기본문법

1) 결과 조회

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne() : 단 건 조회
    (결과가 없으면 : null)
    (결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException)
  • fetchFirst() : limit(1).fetchOne()
  • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
    (count 쿼리가 나가고 content 쿼리가 나감 2번 나간다.)
    (실무에서는 페이징 할 때 카운터 쿼리를 분리 해서 사용해야할 때도 있다)
  • fetchCount() : count 쿼리로 변경해서 count 수 조회

2) 정렬

  • orderBy(.desc() , ~.nullsFirst()): 이 메소드 내에서 아래의 메소드들 사용
  • desc() , asc() : 일반 정렬
  • nullsLast() , nullsFirst() : null 데이터 순서 부여
    (null인 데이터들을 앞으로 땡길지 뒤로 밀지 정하는 거)

3) 페이징

  • offset() : 시작점
  • limit() : 최대 조회 개수
  • 위에 두개로 페이징을 맞춘다.
  • fetchResults() 가 아닌 fetchCount() 사용 해야할 때도 있음
    (페이징 쿼리가 복잡할 때 따로 작성해야함 where 조건에 따라 content쿼리와 count쿼리가 차이가 날 때)

    참고: 실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만,
    count 쿼리는 조인이 필요 없는 경우도 있다. 그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두
    조인을 해버리기 때문에 성능이 안나올 수 있다. count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면,
    count 전용 쿼리를 별도로 작성해야 한다.

4) 집합

  • 앤티티.count() : 회원수
  • 앤티티.속성.sum() : 더하기
  • 앤티티.속성.avg() : 평균
  • 앤티티.속성.max() : 최대
  • 앤티티.속성.min() : 최소
List<Tuple> result = queryFactory
 .select(member.count(),
 member.age.sum(),
 member.age.avg(),
 member.age.max(),
 member.age.min())
 .from(member)
 .fetch();
  • Tuple타입은 단일 타입이 아니라 데이터 타입이 여러개 들어오면 사용한다.
    (나중에 하겠지만 보통 DTO로 직접 뽑아내는 방법을 많이씀)
  • groupBy(): 어떤 기준으로 그룹을 묶을 것 인지 (sql하고 같음)
  • having() :그룹의 조건 (sql하고 같음)
// 아이템의 가격으로 그룹을 짓고 1000원이 넘는 것만 뽑아라 
.
.
.groupBy(item.price)
.having(item.price.gt(1000))
.
.

5) 조인

  • join() , innerJoin() : 내부 조인(inner join)
  • leftJoin() : left 외부 조인(left outer join)
  • rightJoin() : rigth 외부 조인(rigth outer join)
  • JPQL의 on 과 성능 최적화를 위한 fetch 조인 제공 다음 on 절에서 설명
// 예시
 List<Member> result = queryFactory
     .selectFrom(member)
     .join(member.team, team)
     .where(team.name.eq("teamA"))
     .fetch();

세타 조인

( https://nathanh.tistory.com/134 조인에 대하여 다시 복습하려면 여기 블로그 참고 )

  • 연관관계가 없는 필드로 조인
  • form 절에서 그냥 엔티티를 나열한다.
// 예시 (카디션 프로덕트로 나타남)
List<Member> result = queryFactory
      .select(member)
      .from(member, team)
      .where(member.username.eq(team.name))
      .fetch();

5-1) 조인 - on절

  • ON절을 활용한 조인

1. 조인 대상 필터링

  • 예시 코드
/**
 * 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
 * JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'teamA'
 * SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and
t.name='teamA'
 */
@Test
public void join_on_filtering() throws Exception {
     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);
     }
}



========= 결과 출력 ===========
// leftJoin() 으로 사용 했을 떄 
t=[Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
t=[Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
t=[Member(id=5, username=member3, age=30), null]
t=[Member(id=6, username=member4, age=40), null]

// join() 으로 사용 했을 때
t=[Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
t=[Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]

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

2. 연관관계 없는 엔티티 외부 조인 (ON절 사용에 주된 이유가 된다.)

  • 예시 코드
    (. leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다)
일반조인: leftJoin(member.team, team)
on조인: from(member).leftJoin(team).on(xxx)
/**
 * 2. 연관관계 없는 엔티티 외부 조인
 * 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
 * JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
 * SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name
 */
@Test
public void join_on_no_relation() throws Exception {
     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);
     }
}


==========결과 ===========
t=[Member(id=3, username=member1, age=10), null]
t=[Member(id=4, username=member2, age=20), null]
t=[Member(id=5, username=member3, age=30), null]
t=[Member(id=6, username=member4, age=40), null]
t=[Member(id=7, username=teamA, age=0), Team(id=1, name=teamA)]
t=[Member(id=8, username=teamB, age=0), Team(id=2, name=teamB)]
  • 5.1 아래 버전에서는 on 을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 없다.

5-2)조인 - 페치 조인

  • 예시)
    이 메소드를 통해서 Member, Team SQL 쿼리 조인으로 한번에 조회
.join(member.team, team).fetchJoin()

서브 쿼리

  • com.querydsl.jpa.JPAExpressions 사용
    예시)
JPAExpressions
      .select(memberSub.age.avg())
      .from(memberSub)
  • import를 사용하면 JPAExpressions 생략가능
import static com.querydsl.jpa.JPAExpressions.select;

from 절의 서브쿼리 한계

JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl
도 지원하지 않는다. 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. Querydsl도
하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.

from 절의 서브쿼리 해결방안

(DB는 간단하게 데이터를 저장하고 퍼오는 것에만 집중하자 복잡하게 막 만들지마라)

    1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
    1. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
    1. nativeSQL을 사용한다.

Case문

  • DB에서 이렇게 조건을 여러가지 걸고 하는것은 좋지 않다고 본다고 하심.
  • select, 조건절(where), order by에서 사용 가능
    예시)
// 예를 들어서 다음과 같은 임의의 순서로 회원을 출력하고 싶다면?
// 1. 0 ~ 30살이 아닌 회원을 가장 먼저 출력
// 2. 0 ~ 20살 회원 출력
// 3. 21 ~ 30살 회원 출력



NumberExpression<Integer> rankPath = new CaseBuilder()
     .when(member.age.between(0, 20)).then(2)
     .when(member.age.between(21, 30)).then(1)
     .otherwise(3);

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

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


=======결과========
결과
username = member4 age = 40 rank = 3
username = member1 age = 10 rank = 2
username = member2 age = 20 rank = 2
username = member3 age = 30 rank = 1 

상수와 문자 더하기

상수

  • 상수가 필요하면 Expressions.constant(xxx) 사용
  • JPQL에서는 따로 내용이 더해지지 않고 결과에서만 합쳐진다.
    예시)
Tuple result = queryFactory
     .select(member.username, Expressions.constant("A"))
     .from(member)
     .fetchFirst();

for(Tuple tuple : result){
     sout("tupe = " + tuple)
}
=========결과 ========
tuple = [member1, A]
tuple = [member2, A]
tuple = [member3, A]
tuple = [member4, A]

위와 같이 constant가 없어도 최적화가 가능한 SQL에는 constant 값을 넘기지 않는다.
상수를 더해야하는 어려운 상황에서의 최적화가 필요할때만 constant 값을 넘겨준다.

문자 더하기

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

===== 결과 ====
member1_10

: member.age.stringValue() 부분이 중요한데, 문자가 아닌 다른 타입들은 stringValue()를 통해 문자로 변환할 수 있다. 이 방법은 ENUM을 처리할 때도 자주 사용한다.

-중급 문법

1) 프로젝션과 결과 반환 - 기본

  • 프로젝션 이란 select 대상 지정하는 것을 의미
  • 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있음
  • 프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회

튜플 조회

  • 프로젝션 대상이 둘 이상일 때 사용
  • com.querydsl.core.Tuple
List<Tuple> result = queryFactory
  • Tuple을 repository 계층에서만 사용하고 바깥으로 던지는건 DTO로 하자(주로 DTO로 사용하자)

DTO 조회 -순수 JPA만 사용하면

  • JPQL에서 제공하는 new Operation 문법 예시
  • 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야함
  • DTO의 package이름을 다 적어줘야해서 지저분함
  • 생성자 방식만 지원함(setter안된디)

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

     public MemberDto() {
     }

     public MemberDto(String username, int age) {
     this.username = username;
     this.age = age;
     }
}
// JPA에서 DTO 조회 코드 
List<MemberDto> result = em.createQuery(
     "select new study.querydsl.dto.MemberDto(m.username, m.age) " +
         "from Member m", MemberDto.class)
    .getResultList();

DTO 조회 - Querydsl 빈 생성(Projections.~~~)

  • 결과를 DTO 반환할 때 사용
  • 다음 3가지 방법 지원 (위에 순수 JPA하고 비교하면서 보면 편하다.)
1) 프로퍼티 접근
List<MemberDto> result = queryFactory
     .select(Projections.bean(MemberDto.class,
         member.username,
         member.age))
     .from(member)
     .fetch();

2) 필드 직접 접근
List<MemberDto> result = queryFactory
     .select(Projections.fields(MemberDto.class,
         member.username,
         member.age))
     .from(member)
     .fetch();

3) 생성자 사용
List<MemberDto> result = queryFactory
     .select(Projections.contructor(MemberDto.class,
         member.username,
         member.age))
     .from(member)
     .fetch();
  • 프로퍼티나, 필드 접근 생성 방식에서 이름이 다를 때 해결 방안 2가지
    (username이 아닌 name으로 필드를 만듬)
    (위와 3가지 방법으로 조회하면 null로 값이 들어간다. )
    ex)
@Data
public class UserDto {
     private String name;
     private int age;
}
// 1) username.as("memberName") : 필드에 별칭 적용
// 2) ExpressionUtils.as(source,alias) : 필드나, 서브 쿼리에 별칭 적용 (복잡한 방법)

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

1-1) 프로젝션과 결과 반환 - @QueryProjection

  • 생성자 + @QueryProjection
    (생성자를 이용한 DTO 조회가 가능하게 해줌)
    (QMemberDto 생성 확인가능)
@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();
  • 방법은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법
  • DTO에 QueryDSL 어노테이션을 유지해야 하는 점(QueryDSL의 의존성이 생김)과 DTO까지 Q 파일을 생성해야 하는 단점(무거워진다.)

2) 동적 쿼리 해결 방식

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 조건에 null 값은 무시되는 것을 이용한다.
  • 메서드를 다른 쿼리에서도 재활용 할 수 있다.
  • 조합 가능
조합 예제 
private BooleanExpression allEq(String usernameCond, Integer ageCond) {
     return usernameEq(usernameCond).and(ageEq(ageCond));
}
    @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;
    }

3) 수정, 삭제 벌크 연산

  • 쿼리 한번으로 대량 데이터 수정
    (DB로 팍 나가고 영속성컨텍스트 정보는 수정 전으로 남아있음)
    (고로 비워라)
long count = queryFactory
     .update(member)
     .set(member.username, "비회원")
     .where(member.age.lt(28))
     .execute();


-----
기존 숫자에 곱하기: multiply(x)
  • 쿼리 한번으로 대량 데이터 삭제
long count = queryFactory
     .delete(member)
     .where(member.age.gt(18))
     .execute();

4) SQL function 호출하기

  • SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있다.
  • Expressions.stringTemplate("function('함수명', {0}, {1}, {2})", 대상0, 대상1, 대상2)
//member M으로 변경하는 replace 함수 사용
String result = queryFactory
        .select(Expressions.stringTemplate("function('replace', {0}, {1},
{2})", member.username, "member", "M"))
        .from(member)
        .fetchFirst();

//소문자로 변경해서 비교해라.
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()))
profile
일단 흐자

0개의 댓글