[Querydsl] 문법

Dev_Sanizzang·2023년 5월 22일
0

Querydsl

목록 보기
2/3

예제 도메인 모델 설계

Member.java

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {

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

    private void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}
  • Setter 같은 경우는 실무에서 안쓰는게 좋다.

Team.java

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {

    @Id @GeneratedValue
    private Long id;
    private String name;

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

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

기본 문법

JPQL vs Querydsl

  • JPQL
    @Test
    public void startJPQL() {
        //member1을 찾아라.
        Member findMember = em.createQuery("select m from Member m where m.username = : username", Member.class)
                .setParameter("username", "member1")
                .getSingleResult();

        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
  • Querydsl 사용
    // QueryDSL은 컴파일 타임에 오류를 잡을 수 있다.
    // 파라미터 바인딩을 자동으로 해결해 준다.
    @Test
    public void startQuerydsl() {
//        JPAQueryFactory queryFactory = new JPAQueryFactory(em); // 필드로 가져감 (멀티스레드에서도 문제 없게 설계되어있다.)
//        QMember m = new QMember("m");

        // QMember는 static으로 지정 가능 (권장)
        Member findMember = queryFactory
                .select(member) // QMember.member
                .from(member)
                .where(member.username.eq("member1")) // 파라이멑 바인딩 처리
                .fetchOne();

        assertThat(findMember.getUsername()).isEqualTo("member1");
    }

Q 클래스 인스턴스 사용하는 2가지 방법

QMember qMember = new QMember("m"); // 별칭 직접 지정
QMember qMember = QMember.member; // 기본 인스턴스 사용
  • QMember.member -> static으로 지정 가능하다. (권장)

Querydsl은 결국 jpql 빌더 역할을 하는데 jpql 문법을 로그로 확인하고 싶다면 use_sql_comments를 추가하면된다.

💡 나머지 기본문법 코드는 너무 많은 관계상 github의 코드를 확인하고 중요한 부분만 정리해놓겠다.

on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인을 사용하면, where절에서 필터링 하는 것과 기능이 동일하다.

    /**
     * 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
     * JPQL: select m, t from Member m left join m.team t on t.name = 'teamA'
     */
    @Test
    public void join_on_filtering() {
        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .join(member.team, team)
                //.on(team.name.eq("teamA"))
                .where(team.name.eq("teamA"))
                .fetch();

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

서브 쿼리 같은 경우 안과 밖 alias가 달라야 한다.

@Test
    public void subQuery() {

        QMember memberSub = new QMember("memberSub");

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

        assertThat(result).extracting("age")
                .containsExactly(20, 30, 40);
    }
  • com.querydsl.jpa.JPAExpresions 사용
  • member, memberSub으로 다른 alias 사용

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

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

서브쿼리는 높은 확률로 join으로 바꿀 수 있다.
성능이 엄청 중요한 시스템이 아닌이상 query를 두 번 이상 사용해도 된다.(상황에 따라 다름)

SQL은 데이터를 가져오는 것에 집중을하고 필요하면 애플리케이션에서 로직을 태우는 것이 좋다.
(Query를 너무 복잡하게 할 필요가 없다. (물론 WHERE와 GROUP BY를 통해 잘라내는 것도 중요하다.))

💡 하이버네이트 6부터는 From절에서의 서브쿼리를 지원한다고 한다.
아직은 공식적으로 지원해주지는 않고 있는 것으로 보여 서드파티를 적용해야 할 것 같다고 한다

중급 문법

프로젝션: select 대상 지정

@Test
    public void simpleProjection() {
        List<String> result = queryFactory
                .select(member.username)
                .from(member)
                .fetch();

        for (String s : result) {
            System.out.println("s = " + s);
        }
    }
  • 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다.
  • 프로젝션 대상이 둘 이상이면 "튜플"이나 "DTO"로 조회

튜플 사용

    @Test
    public void tupleProjection() {
        // Tuple -> package com.querydsl.core
        // Tuple을 Repository 계층 안에서 사용하는 것은 괜찮은데 Service나 Cotroller까지 넘어가는 것은 좋은 설계가 아니다.
        // 하부 기술을 다른 곳에 노출 시키면 변경이 힘들다.
        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);
        }
    }

DTO 사용

  • JPQL에서의 Dto 조회
    @Test
    public void findDtoByJPQL() {
        // 순수 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();

        for (MemberDto memberDto : result) {
            System.out.println("memberDto = " + memberDto);
        }
    }

💡 Querydsl 빈 생성
: 결과를 DTO 반환할 때 사용
다음 3가지 방법 지원

  • 프로퍼티 접근
  • 필드 직접 접근
  • 생성자 사용
    @Test
    public void findDtoBySetter() {
        List<MemberDto> result = queryFactory
                .select(Projections.bean(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();

        for (MemberDto memberDto : result) {
            System.out.println("memberDto = " + memberDto);
        }
    }

위의 방법을 사용하려면 해당 DTO에 기본생성자가 있어야 한다.
-> Querydsl이 해당 DTO를 만든 다음 그 값에 Set을 해야하는데 기본생성자가 없다면 오류가 뜬다.

@QueryProjection

@QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
  • @QueryProjection을 붙여주면 DTO도 Q파일이 생성된다.
    @Test
    public void findDtoByQueryProjection() {
        List<MemberDto> result = queryFactory
                // QueryProjection을 사용하면 컴파일 에러로 확인이 가능하다.
                // 단점?
                // MemberDto 자체가 QueryDSL에 의존성을 가지게 된다.
                .select(new QMemberDto(member.username, member.age))
                .from(member)
                .fetch();

        for (MemberDto memberDto : result) {
            System.out.println("memberDto = " + memberDto);
        }
    }

동적 쿼리 해결법

  • BooleanBuilder
	@Test
    public void dynamicQuery_BooleanBuilder() {
        String usernameParam = "member1";
        Integer ageParam = 10;

        List<Member> result = searchMember1(usernameParam, ageParam);
        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 다중 파라미터 사용
    @Test
    public void dynamicQuery_WhereParam() {
        String usernameParam = "member1";
        Integer ageParam = 10;

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

    private List<Member> searchMember2(String usernameCond, Integer ageCond) {
        return queryFactory
                .selectFrom(member)
                // where 가 만약 null이라면 값이 무시가 된다.
                .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;
    }
  • BooleanBuilder보다 Where 다중 파라미터 사용이 더 깔끔하게 쿼리를 작성할 수 있다. (가독성이 높다.)
  • 메소드로 쿼리 조건이 빠졌기 때문에 아래와 같이 조립도 가능하다. (메서드 재활용)
    private BooleanExpression allEq(String usernameCond, Integer ageCond) {
        return usernameEq(usernameCond).and(ageEq(ageCond));
    }

벌크 연산

    @Test
    public void bulkUpdate() {
        long count = queryFactory
                .update(member)
                .set(member.username, "비회원")
                .where(member.age.lt(28))
                .execute();

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

💡 벌크 연산은 조심해야될게 있다.
벌크 연산은 영속성 컨텍스트를 무시하고 DB에 바로 Query가 날라간다 (DB의 상태와 영속성 컨텐스트의 상태가 달라진다)
-> 고로 벌크 연산 수행 시 영속성 컨텍스트를 flush 후 초기화 하도록 하자

🚪 마무리

이번 포스팅에서는 Querydsl의 문법들을 정리해봤다. 다음 포스팅에서는 순수 JPA와 Querydsl Spring data JPA와 Querydsl에 대해서 알아보도록 하자.

profile
기록을 통해 성장합니다.

0개의 댓글