Querydsl 사용

Chooooo·2023년 9월 12일
0

Querydsl

목록 보기
3/3
post-thumbnail

저번에 Querydsl 설정을 알아보았다. 이제 직접 사용해보자.

😎 Querydsl 사용

😊 JPAQueryFactory 빈 등록

JpaRepository를 custom 했다는 컨벤션으로 ~~ RepositoryCustom을 만들어 해당 Repository를 상속하고, ~~ Impl로 구현체로 만드는 방식을 선택할 수도 있습니다. 하지만 Querydsl을 사용하기 위한 JPAQueryFactory를 Bean으로 만들고 @Repository 어노테이션을 사용하시는 게 더 좋습니다.

ex)

  • MemberRepository extends JpaRepository
  • MemberRepositoryCustom extends MemberRepository
  • MemberRepositoryImpl implmentation MemberRepositoryCustom

위처럼 사용하지 않습니다.

아래처럼 @Bean으로 등록해서 사용하면 됩니다.

@Configuration
@RequiredArgsConstructor
public class QuerydslConfig {
 
    private final EntityManager em;
    @Bean
    public JPAQueryFactory jpaQueryFactory(EntityManager em){
        return new JPAQueryFactory(em);
    }
}
@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl{
	
    private final JPAQueryFactory factory;
    
    // 중략
 
}

😊 Query 사용

Test 코드를 작성하는 곳에 시작합니다. **세팅**을 먼저 해줍니다. 주석으로 설명을 달겠습니다.

😊 Q객체 사용

아래의 엘리어스(별칭)를 주면서 Q객체를 사용할 수 있습니다.

QMember member = new QMember("member");

🎈 하지만 Q객체들은 static으로 접근할 수 있으므로 아래와 같이 같은 Q객체를 여러번 사용하지 않는 이상 static을 import 해서 사용합니다.

QMember.member;
// QMember를 static import
⬇
member;
  • Querydsl의 장점.. 그냥 멤버처럼 사용할 수 있게 됐다 !

😊 select 쿼리

아래와 같이 jpaQueryFactory의 메서드들을 이용합니다. 메서드들의 이름으로 직관적으로 어떠한 역할을 하는지 알 수 있습니다. selectfrom을 나눠서 작성할 수도 있고, 합쳐서 selectFrom으로 작성할 수도 있습니다.

@Test
void select(){
 
    Member result1 = jpaQueryFactory.select(member)
            .from(member).fetchOne();
 
    Member result2 = jpaQueryFactory.selectFrom(member)
            .fetchOne();
}

😊 return Result

위에서 fetchOne()을 붙여서 단 건 조회를 했습니다. 아래와 같이 반환인자를 결정할 수 있습니다.

🎈 fetchOne(): 단건 조회 여러 개가 조회되면 오류 발생
🎈 fetchFirst() : 단 건 조회이 여러 개가 조회돼도 첫 번째 값 반환
🎈 fetch() : List 형태로 반환

😓 fetchResult, fetchCount: 두 개가 deprecated되었습니다. 각각의 역할을 할 수 있는 방법은 밑에서 페이징 관련하면서 알아보겠습니다.

@Test
void returnResult(){
    Member result1 = jpaQueryFactory.selectFrom(member)
            .where(member.name.eq("hi1"))
            .fetchOne();
 
    Member result2 = jpaQueryFactory.selectFrom(member)
            .fetchFirst();
 
    List<Member> result3 = jpaQueryFactory.selectFrom(member)
            .fetch();
}

😁 연산

🤭 eq 연산

  • JPA의 명명법으로 사용했던 쿼리를 querydsl로 사용하는 방법입니다. where(컬럼.eq(조건)) 이고, eq는 ==이라고 생각하시면 됩니다.
@Test
void findName(){
 
    Member result = jpaQueryFactory.select(member)
            .from(member)
            .where(member.name.eq("hi1"))
            .fetchOne();
    assertThat(result.getAge()).isEqualTo(10);
}
  • and 혹은 ,를 사용해서 조건 여러 개를 설정할 수 있습니다.
@Test
void findNameAndAge(){
    Member result1 = jpaQueryFactory.select(member)
            .from(member)
            .where(member.name.eq("hi1").and(member.age.eq(10)))
            .fetchOne();
 
    Member result2 = jpaQueryFactory.select(member)
            .from(member)
            .where(member.name.eq("hi1"),member.age.eq(10))
            .fetchOne();
 
    assertThat(result1.getAge()).isEqualTo(10);
    assertThat(result2.getAge()).isEqualTo(10);
}

🎈 where 조건 안에서 ,로 구분하는 것이 깔끔 !!

🤭 ne 연산

  • 위에서 == 을 알아봤다면 ne는 != 입니다. 예제는 이름이 "hi1"이 아닌 Member들을 조회하므로 사이즈가 3입니다.
@Test
void notEqual(){
    List<Member> results = jpaQueryFactory.selectFrom(member)
            .where(member.name.ne("hi1")).fetch();
 
    assertThat(results.size()).isEqualTo(3);
}

🤭 in 연산

  • in절은 in() 메서드 안에 값들을 넣어주면 됩니다. 예제는 나이가 10,11인 Member를 찾는 것입니다. 2명이 조회됩니다.
@Test
void in(){
    List<Member> results = jpaQueryFactory.selectFrom(member)
            .where(member.age.in(10, 11)).fetch();
 
    assertThat(results.size()).isEqualTo(2);
}

🤭 like 연산

like연산은 "%3"이라면 3으로 끝나는 것을 조회하는 것입니다. 반대로 "3%"라면 3으로 시작하는 것

예제에서는 "%3"으로 해서 이름이 3으로 끝나는 멤버를 찾는 것입니다.
@Test
void like(){
    Member result = jpaQueryFactory.selectFrom(member)
            .where(member.name.like("%3")).fetchOne();
 
    assertThat(result.getName()).isEqualTo("hi3");
}

🤭 contains 연산

  • contains는 포함하는지 확인하는 연산입니다. 즉 like에서 "%3%"으로 한다면 같은 결과를 반환합니다.
@Test
void contains(){
    Member result = jpaQueryFactory.selectFrom(member)
            .where(member.name.contains("3"))
            .fetchOne();
 
    assertThat(result.getName()).isEqualTo("hi3");
}

🤭 정렬(sort)

  • orderBy를 이용해서 정렬 조건을 줄 수 있습니다. 예제에서는 나이를 내림차순으로 null값을 맨 뒤로 보내는 정렬입니다.

  • orderBy를 여러 개 사용함으로써 정렬 조건을 추가할 수 있습니다.

@Test
void sort(){
    List<Member> fetch = jpaQueryFactory.selectFrom(member)
            .orderBy(member.age.desc().nullsLast())
            //. orderBy(member.name.asc())
            .fetch();
 
    assertThat(fetch.get(0).getAge()).isEqualTo(13);
}

🤭 페이징(page)

  • limitoffset을 이용해서 페이징을 할 수 있습니다. 여기서 limit은 페이지의 크기이고, offset은 시작점입니다.

  • 예제에서는 나이를 내림 차순으로 정렬하고, 0번째 인덱스부터 시작하는데 2개씩 자르는 것입니다.

  • 13 12 | 11 10로 페이지가 나눠진 형태이고, 13 12가 선택된 것입니다.

@Test
void page(){
    List<Member> result = jpaQueryFactory.selectFrom(member)
            .orderBy(member.age.desc())
            .offset(0)
            .limit(2)
            .fetch();
 
    assertThat(result.size()).isEqualTo(2);
    assertThat(result.get(0).getName()).isEqualTo("hi4");
}

내장함수 사용

  • querydsl에서는 데이터베이스에서 사용하는 대부분의 내장 함수를 사용할 수 있습니다. 예제는 sum, avg, count, min, max를 구하는 것입니다. tuple에 들어있는 값을 얻기 위해선 select에서 사용한 명칭을 그대로 사용하면 됩니다.
@Test
void aggregation(){
    List<Tuple> tuples = jpaQueryFactory.select(member.age.sum(),
            member.age.avg(),
            member.age.count(),
            member.age.max(),
            member.age.min()).from(member)
            .fetch();
 
    assertThat(tuples.size()).isEqualTo(1);
    Tuple result = tuples.get(0);
 
    assertThat(result.get(member.age.count())).isEqualTo(4);
    assertThat(result.get(member.age.max())).isEqualTo(13);
    assertThat(result.get(member.age.min())).isEqualTo(10);
}

🤭 groupBy having

  • groupBy 메서드와 having 메서드를 그대로 사용합니다. 예제에서는 Member의 나이별로 묶었고 having에서 주어진 조건은 age별로 묶었을 때 개수가 2보다 크거나 같은 것들을 조회하는 것입니다. 따라서 나이가 10인 그룹만 조회됐습니다.
@Test
void groupBy(){
    memberRepository.save(createMember("hi5",10));
    memberRepository.save(createMember("hi6",10));
    List<Long> result = jpaQueryFactory.select(member.age.count())
            .from(member)
            .orderBy(member.age.asc())
            .groupBy(member.age)
            .having(member.age.count().goe(2))
            .fetch();
 
    assertThat(result.get(0)).isEqualTo(3);
    assertThat(result.size()).isEqualTo(1);
}

🤭 조인(join)

  • join 메서드를 사용합니다. join(조인할 대상, 조인 별칭) 입니다. 예제에서는 Member의 club을 조인하고, club의 이름이 smu인 것을 조회합니다.
@Test
void join(){
    List<Member> result =jpaQueryFactory.selectFrom(member)
            .join(member.club, club)
            .where(club.name.eq("smu"))
            .fetch();
 
}

🎈 패치 조인을 사용하려면 뒤에 패치 조인만 붙여주면 됩니다.

@Test
void fetchJoin() {
    List<Member> result = jpaQueryFactory.selectFrom(member)
            .join(member, club).fetchJoin()
            .where(club.name.eq("smu"))
            .fetch();
}

🎈 세타 조인도 지원합니다.

@Test
void thetaJoin(){
    List<Member> result =jpaQueryFactory.selectFrom(member)
            .join(member,club)
            .where(member.name.eq(club.name))
            .fetch();
}

🎈 on 조인도 지원합니다.

@Test
void onJoin(){
    List<Tuple> result =jpaQueryFactory.selectFrom(member)
            .join(member,club)
            .leftJoin(group).on(member.name.eq(group.name))
            .fetch();
}

🤭 서브 쿼리

  • 서브 쿼리는 from 절을 제외하고, select, where절에서 사용할 수 있습니다. JPAExpressions를 이용합니다. 이때 같은 테이블을 서브 쿼리에서 사용한다면 위에서 알아봤었던 별칭을 이용해서 별도로 Q객체를 하나 더 만들어야 합니다.

  • 예제에서는 서브 쿼리를 이용해서 가장 나이가 많은 멤버를 찾는 것입니다. JPAExpressions도 static import해서 사용하면 됩니다.

@Test
void subQuery() {
 
    QMember subMember = new QMember("subMember");
 
    List<Member> result = jpaQueryFactory.selectFrom(member)
            .where(member.age.eq(JPAExpressions.select(subMember.age.max()).from(subMember)))
            .fetch();
 
    assertThat(result.get(0).getAge()).isEqualTo(13);
}

🤭 Case

case문을 when, then으로 사용할 수 있습니다. 컬럼.when(조건).then(행동).whe(조건).then(행동).otherwise(행동)

예제에서는 Member의 나이를 가지고 age=10 은 10살, age=11은 11살 그 외에 기타로 했습니다.

@Test
void caseWhen() {
    List<String> result = jpaQueryFactory.
            select(member.age
                    .when(10).then("10살")
                    .when(11).then("11살")
                    .otherwise("기타"))
            .from(member)
            .orderBy(member.name.asc()).fetch();
 
    assertThat(result.get(3)).isEqualTo("기타");
 
}

간단한 조건이 아닌 복잡한 조건을 가질 때는 CaseBuilder를 사용합니다. 위의 예제를 조금 변형해서 10 <=age <=11일 때는 10~11살 나머지는 기타로 하기 원한다면 아래와 같이 작성해주시면 됩니다.

@Test
void caseWhen2() {
    List<String> result = jpaQueryFactory
            .select(new CaseBuilder()
                    .when(member.age.between(10, 11)).then("10~11살")
                    .otherwise("기타"))
            .from(member)
            .orderBy(member.name.asc()).fetch();
 
    assertThat(result.get(3)).isEqualTo("기타");
}

🤭 상수(const)

상수를 선언하고 싶다면 Expressions.constant 메서드를 사용합니다. 예제에서는 상수 "A" 만들도록 하겠습니다. 마찬가지로 static import 하셔서 사용하면 편리합니다.

@Test
void constEx() {
    Tuple tuple = jpaQueryFactory
            .select(member.name, Expressions.constant("A"))
            .from(member)
            .fetchFirst();
 
    assertThat(tuple.get(member.name)).isEqualTo("hi1");
    assertThat(tuple.get(Expressions.constant("A"))).isEqualTo("A");
}

🤭 concat

문자열을 별도로 붙이고 싶다면 concat을 이용합니다. 이때 String이 아니라면 stringValue()를 사용하셔야 합니다. enum, 숫자, 날짜 등등에 사용할 수 있습니다. 예제에서는 이름+_+나이로 바꿨습니다.

@Test
void concat() {
    String result = jpaQueryFactory
            .select(member.name.concat("_").concat(member.age.stringValue()))
            .from(member)
            .where(member.name.eq("hi1"))
            .fetchOne();
 
    assertThat(result).isEqualTo("hi1_10");
}

🤭 projection

JPA에서는 프로젝션을 하기 위해선 디렉터리 명을 모두 적었어야 했습니다. 하지만 querydsl에서는 쉽게 사용할 수 있습니다. Entity를 반환하는 것은 필요 없는 필드 값들도 있으므로 성능 저하가 있을 수 있습니다. 따라서 프로젝션을 사용하는 것이 성능 면에서 이로울 수 있습니다.

  • 먼저 프로젝션 할 객체를 선언합니다.
@Data
@NoArgsConstructor
public class Dto {
 
    public String username;
    public int age;
 
 
    @QueryProjection
    public Dto(String username, int age){
        this.username=username;
        this.age=age;
    }
}

🎈 이제 프로젝션 방법에 대해서 알아보겠습니다.

🎈 1 .setter 접근

Projections.bean을 이용하면 setter로 접근해서 필드 값들을 채워줍니다. Entity의 필드명과 Dto의 필드명이 같다면 그대로 사용해도 되지만, 필드명이 다르다면 as를 사용해야 합니다. 예제에서는 member에서는 name, Dto에서는 username이라 as를 사용합니다.

Dto result1 = jpaQueryFactory
        .select(Projections.bean(Dto.class,
                member.name.as("username"),
                member.age))
        .from(member)
        .where(member.name.eq("hi1"))
        .fetchOne();

🎈 2. 필드 직접 접근

Projections.field를 사용하면 필드에 직접 접근할 수 있습니다.

Dto result2 = jpaQueryFactory
        .select(Projections.fields(Dto.class,
                member.name.as("username"),
                member.age))
        .from(member)
        .where(member.name.eq("hi1"))
        .fetchOne();

🎈 3. 생성자 접근

Projections.constructor를 사용하면 생성자를 이용해서 접근합니다.

Dto result3 = jpaQueryFactory
        .select(Projections.constructor(Dto.class,
                member.name.as("username"),
                member.age))
        .from(member)
        .where(member.name.eq("hi1"))
        .fetchOne();

🎈 궁극의 방법 : @QueryProjection

프로젝션 할 객체의 생성자 위에 @QueryProjection을 붙이면 해당 객체 또한 Q객체를 생성합니다. 따라서 new를 이용해서 프로젝션 진행이 가능합니다. 간단하지만, Dto에 코드 침투가 일어나므로 해당 부분은 고려해야 할 사항입니다.

Dto result4 = jpaQueryFactory
        .select(new QDto(member.name, member.age))
        .from(member)
        .where(member.name.eq("hi1"))
        .fetchOne();

🤭 distinct

  • distinct 메서드를 이용해 적용할 수 있습니다.
@Test
void distinct() {
    List<String> fetch = jpaQueryFactory.select(member.name).distinct()
            .from(member)
            .fetch();
}

🤭 동적 쿼리 : Querydsl의 꽃

🎈 JPQL에서는 동적 쿼리를 문자열 방식으로 처리해 많은 불편함이 있었습니다. querydsl에서는 BooleanBuilderWhere절 다중 파라미터를 이용해서 처리할 수 있습니다. 더 많이 사용하는 방식인 where절 다중 파라미터에 대해서만 알아보겠습니다.

🎈 where절에 BooleanExpreesion을 반환하는 메서드를 사용합니다. 그리고 nameEq, ageEq에서는 값이 없다면 null을 반환하고, 아닐 경우에는 값을 반환합니다. where절에서는 null 값인 경우에 조건을 무시합니다. 따라서 동적 쿼리를 완성할 수 있습니다.

@Test
void dynamicQuery() {
    List<Member> hi1 = findUser("hi1", 10);
    assertThat(hi1.size()).isEqualTo(1);
}
 
private List<Member> findUser(String nameCondition, Integer ageCondition) {
    return jpaQueryFactory
            .selectFrom(member)
            .where(nameEq(nameCondition), ageEq(ageCondition))
            .fetch();
}
 
private BooleanExpression nameEq(String nameCondition) {
    return nameCondition != null ? member.name.eq(nameCondition) : null;
}
 
private BooleanExpression ageEq(Integer ageCondition) {
    return ageCondition != null ? member.age.eq(ageCondition) : null;
}

🤭 update

update의 경우 update set을 사용하고 마지막에 execute를 붙여 바뀐 개수를 반환받습니다. 이때 주의할 점은 bulk 연산은 영속성 콘텍스트가 아닌 바로 데이터베이스에 접근하므로 영속성 콘텍스트에 남아있는 변경 사항과 값들을 데이터 베이스에 반영하고 초기화해줘야 합니다.

@Test
void update() {
    long count = jpaQueryFactory
            .update(member)
            .set(member.age, member.age.add(10))
            .where(member.name.eq("hi1"))
            .execute();
 
    em.flush();
    em.clear();
 
}

🤭 utilPage

조회된 데이터의 내용과 전체 개수를 구하는 쿼리는 따로 작성하는 것이 성능상 좋습니다.

  • 그 이유는 카운트를 세는 쿼리의 경우 조인 연산이 불필요하기 때문입니다. 따라서 조회 쿼리와 개수를 세는 쿼리를 분리합니다.

여기서 이전에는 개수까지 한 번에 세는 fetchResults를 사용하는 것은 성능에 영향을 끼칠 수도 있어서 사용하지 않기도 하고, 모든 쿼리에 대해서 개수를 정확하게 내지 않아서 deprecated 되었습니다.

  • 마찬가지로 fetchCount는 개수를 세는 메서드입니다. deprecated 되었습니다.

🎈 따라서 fetchResult가 아닌 fetch를 사용하고, fetchCount가 아닌 fetch(). size()를 사용하라고 주석으로 달려있습니다.

fetch(). size()가 아닌 Wildcart.count를 활용하는 것이 더 깔끔합니다.

이렇게 해서 데이터와 전체 개수를 구했다면, PageableExecutonUtils를 이용합니다. 이것은 Data-JPA에서 제공해주는 함수입니다. 아래와 같은 역할을 해줍니다.

count 쿼리가 생략 가능한 경우 생략해서 처리

    1. 페이지 시작이면서 콘텐츠 사이즈가 페이지 사이즈보다 작을 때
    1. 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)

따라서 아래와 같이 최적화를 진행할 수 있습니다.

@Test
void utilPage() {
    PageRequest pageable = PageRequest.of(0, 2);
 
    List<Member> content = jpaQueryFactory
            .selectFrom(member)
            .limit(pageable.getPageSize())
            .offset(1)
            .fetch();
 
 
    Long totalCount = jpaQueryFactory.select(Wildcard.count)
            .from(member)
            .fetch().get(0);
 
    Page<Member> page = PageableExecutionUtils.getPage(content, pageable, () -> totalCount);
 
    List<Member> results = page.getContent();
    int totalPages = page.getTotalPages();
    boolean hasNext = totalPages > pageable.getPageNumber();
 
    assertThat(hasNext).isTrue();
    assertThat(totalPages).isEqualTo(2);
    assertThat(results.size()).isEqualTo(2);
}
profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글