중급 문법

OneTwoThree·2023년 8월 11일
0

실전querydsl

목록 보기
4/6

출처


프로젝션, 결과 반환

프로젝션은 select 대상을 지정하는 것이다.
프로젝션 대상이 컬럼 하나일때는 타입을 명확하게 지정할 수 있어서 타입을 맞추면 된다.
프로젝션 대상이 둘 이상이면 튜플이나 dto로 조회해야 한다.

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

            for (String s : result) {
                System.out.println("s = " + s);
            }
        }

위와 같이 단일 타입에 대해 조회할 때는 해당 타입에 맞춰 반환받으면 된다.

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

Tuple을 사용하면 위와 같이 tuple.get(memer.username)과 같은 문법으로 원하는 필드를 꺼낼 수 있다.

Tuple을 리포지토리에서 사용하는 것은 괜찮지만 서비스, 컨트롤러 계층까지 넘어가서 사용하는 것은 좋은 설계는 아니다.

하부 구현 기술을 앞단에서 알 필요가 없기 때문이다.
그래야 리포지토리를 추후에 교체하더라도 서비스, 컨트롤러에 변경이 없기 때문이다.

Tuple을 바깥계층으로 던지는 것은 리포지토리 내에서 변환한 후에 dto로 바꾸는 것을 권장한다.

JPQL dto 조회

순수 JPA에서 dto를 사용해서 조회할 수 있다.

@Data
@NoArgsConstructor
public class MemberDto {

    private String username;
    private int age;

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

위와 같은 dto를 만들고 JPQL로 조회하려면

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

이렇게 JPQL에서 제공하는 new operation 문법을 사용해야 한다.

querydsl dto 조회

querydsl은 3가지 방법을 지원한다

  • 프로퍼티 접근
  • 필드 직접 접근
  • 생성자 사용
        @Test
        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);
            }

        }

위와 같이 Projections.bean(dto클래스, 조회할 필드, 조회할 필드 , .. ) 문법으로 setter 방식으로 dto에 주입할 수 있다.
이 경우 기본 생성자, setter가 필요하다
dto의 필드명과 엔티티의 필드명이 같아야 한다.

    @Test
    void findDtoByField(){
        List<MemberDto> result = queryFactory
                .select(Projections.fields(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();

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

fields를 사용하면 필드에 바로 값을 주입해준다. 이 경우 setter가 필요없다.
필드 접근 방식은 엔티티티의 필드명과 dto의 필드명이 일치해야 한다.

    @Test
    void findDtoByConstructor(){
        List<MemberDto> result = queryFactory
                .select(Projections.constructor(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();

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

dto로 조회할 때 constructor로 조회할 수도 있다. 이 Projections.constructor를 사용하면 되고 조회하려는 필드와 dto의 필드 타입이 맞아 떨어져야 한다.


    @Test
    void findUserDtoByField(){
        List<UserDto> result = queryFactory
                .select(Projections.fields(UserDto.class,
                        member.username.as("name"),
                        member.age))
                .from(member)
                .fetch();

        for (UserDto userDto : result) {
            System.out.println("userDto = " + userDto);
        }
    }

setter 방식, 필드 방식은 엔티티의 필드명과 dto의 필드명이 일치하지 않으면 값이 null로 들어간다. 매칭이 안되기 때문에 그렇다.
만약 UserDto가 있고 username이 아닌 name 필드로 이름 필드를 만들었다면 프로젝션 할 때
member.username.as("name")과 같이 프로젝션 해주면 된다.

    @Test
    void findUserDtoByField(){
        QMember memberSub = new QMember("memberSub");
        List<UserDto> result = queryFactory
                .select(Projections.fields(UserDto.class,
                        member.username.as("name"),
                        ExpressionUtils.as(JPAExpressions
                                .select(memberSub.age.max())
                                .from(memberSub), "age"
                        )
                        ))
                .from(member)
                .fetch();

        for (UserDto userDto : result) {
            System.out.println("userDto = " + userDto);
        }
    }

또한 프로젝션 할 때 서브쿼리를 사용할 수 있다.
이 경우 필드명을 맞추기 위해 ExpressionUtils를 사용해서 alias를 dto 필드명에 맞게 주면 된다.

그리고 생성자를 사용하는 경우에는 필드명이 아니라 타입으로 매칭되기 때문에 상관없다.

@QueryProjection

제일 깔끔한 해결책이지만 단점도 있다.

@Data
@NoArgsConstructor
public class MemberDto {

    private String username;
    private int age;

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

MemberDto의 생성자에 다음과 같이 @QueryProjection을 달아주면 된다.
그리고 comipleQuerydsl을 해주면 dto 또한 Qtype으로 생성된다.

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

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

위와 같이 new로 QMemberDto를 생성해주고 생성자에 알맞게 넣어주기만 하면 된다.

생성자를 사용하는 방법과 다른 점이 뭘까?
생성자를 사용하는 경우 dto에 정의되지 않은 member.id를 프로젝션에 추가하면 컴파일 시점에 오류를 잡지 못하고 런타임에 오류를 잡는다.

반면에 QMemberDto를 사용하면 똑같은 문제를 컴파일오류로 확인할 수 있다.

하지만 단점도 있다

  • QType을 생성해야 한다
  • dto가 querydsl에 의존하게 된다

동적 쿼리

동적 쿼리는 두가지 방법으로 해결할 수 있다.
BooleanBuilder와 where 다중 파라미터를 사용하는 방식이다.

BooleanBuilder 사용

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

조건이 null이 아니면 where절에 조건이 들어가고 null이면 조건이 들어가면 안되는 상황이다.
이렇게 동적 쿼리가 필요한 상황에서는 BooleanBuilder를 사용해서 동적 쿼리를 짤 수 있다. BooleanBuilder는 and나 or 같은 조건을 조립할 수 있도록 해준다.

만약 무조건 null이 아닌 값이 있다면 BooleanBuilder의 생성자에 초기값으로 넣을 수도 있다.
new BooleanBuilder(member.username.eq(usernameCond)
그리고 builder 자체도 and, or로 조립할 수 있다.

where 문 다중 파라미터 사용

이 방식을 쓰면 훨씬 깔끔하게 동적 쿼리를 작성할 수 있다.

    @Test
    void dynamicQuery_WhereParam(){
        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 Predicate usernameEq(String usernameCond) {
        if (usernameCond==null){
            return null;
        }
        return member.username.eq(usernameCond);
        }

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

where에 null 조건이 들어가면 그냥 무시되기 때문에 동적 쿼리를 작성할 수 있다.
삼항 연산자로 더 간단하게 작성할 수 있다.

Booleanbuilder는 코드가 지저분한 데 비해 이 방식은 메소드 이름으로 조건을 확인할 수 있고 깔끔하다.

이렇게 하면 더 좋은 점은 조건끼리도 조립을 할 수 있다.

    @Test
    void dynamicQuery_WhereParam(){
        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) {
        if (usernameCond==null){
            return null;
        }
        return member.username.eq(usernameCond);
        }

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

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

메소드 반환형을 BooleanExpression으로 하면 함수의 반환형끼리 .and() 등으로 조립을 할 수 있어서 매우 편리하다.
또, 메소드를 다른 쿼리에서 재활용 할 수도 있다.

수정, 삭제 벌크 연산

쿼리 한 번으로 대량의 데이터를 수정할 때 사용한다

    @Test
    public void bulkUpdate(){
        // count는 영향을 받은 row 수
        long count = queryFactory
                .update(member)
                .set(member.username, "비회원")
                .where(member.age.lt(28))
                .execute();
    }

벌크 연산은 영속성 컨텍스트를 무시하고 DB에 바로 쿼리가 나간다
즉 db와 영속성 컨텍스트의 상태가 달라진다

이때 주의할 점은 db에서 select 해와도 db값과 영속성 컨텍스트의 값이 다르면 db에서 가져온 값을 버린다(영속성 컨텍스트가 우선권을 갖는다)

그래서 항상 벌크 연산을 수행하고 나서는
em.flush()
em.clear()를 해주는 것이 좋다(해주자)

즉 벌크 연산을 수행하면 영속성 컨텍스트를 초기화 해 주자
.


   @Test
   public void bulkAdd(){
       queryFactory
               .update(member)
               .set(member.age, member.age.add(1))
               .execute();
   }

이렇게 기존 값에 더할 수 있다.
곱하기는 multiply()


    @Test
    public void bulkDelete(){
        long count = queryFactory
                .delete(member)
                .where(member.age.gt(18))
                .execute();
    }

삭제 연산은 위와 같다

SQL Function 호출하기

해당 내용은 추후에 복습할것

0개의 댓글