Querydsl 중급 문법

Jaca·2021년 9월 24일
0

프로젝션

프로젝션이란, SELECT 절에 조회할 대상을 지정하는 것 이다.
select문으로 검색하는 내용이 무엇이냐는 것!

List<String> result = queryFactory
            .select(member.username)
            .from(member)
            .fetch();

위 코드는 프로젝션이 String을 대상으로 하나 뿐이다.
프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다.
Member 클래스를 조회하는 것도 프로젝션이 대상이 하나라고 한다.

프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회해야한다.

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에서 원하는값을 뽑아 써야한다.

이 튜플을 말고 DTO로 뽑아 쓸 수도 있다.
JPQL의 경우 DTO로 뽑으려면 new 연산자를 사용하여 DTO의 package이름을 다 적어줘야해서 지저분하다.

Querydsl의 DTO 반환은 3가지 방식을 지원한다.

프로퍼티 접근 - setter

List<MemberDto> result = queryFactory
          .select(Projections.bean(MemberDto.class,
                  member.username,
                  member.age))
          .from(member)
	  .fetch();

당연히 setter가 열려있어야 한다.

필드 직접 접근

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

생성자 사용

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

파라미터에 맞는 생성자가 있어야 한다.

속성의 이름이 다를 경우

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

ExpressionUtils.as(source,alias) : 필드나, 서브 쿼리에 별칭 적용
username.as("memberName") : 필드에 별칭 적용

Member의 username과 DTO의 name의 이름이 달라 as로 맞춰준 것,
앞에서 보았듯 서브쿼리에 사용되는 Qclass의 인스턴스는 내외부가 달라야 한다.

위 DTO 반환을 어노테이션으로 해결할 수도 있는데..

@Data
@NoArgsConstructor
public class MemberDto {

    private String username;
    private int age;

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

위와 같이 생성자에 @QueryProjection 어노테이션을 달고,
Qclass를 새로 빌드하면, 어노테이션이 달린 Qclass가 생성된다.

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

그러면 무려 위와같이 간단해진다.
이 방법은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법이다. 다만 DTO에 Querydsl 어노테이션을 유지해야 하는 점과 DTO까지 Q파일을 생성해야 하는 단점이 있다.

이 방법을 사용하기 위해선 Querydsl를 추후에 사용하지 않게될 가능성이 있는지.. 고민 해봐야한다고 한다.
Querydsl에 의존적이기 때문에!.

동적 쿼리

드디어, 동적 쿼리의 처리 부분을 알아보자.

동적 쿼리는 두가지 방식으로 처리할 수 있다.

BooleanBuilder

BooleanBuilder 클래스를 설정하고
들어온 파라미터들의 null 여부를 검사하여, 조건을 달아준다.
앞서 배웠듯 Querydsl은 null이 들어오면 무시하기 때문에.
JPQL과 차이가 있다.

where 다중 파라미터

where문에 각 파라미터가 null인지 체크 하는 부가 메서드를 생성한다.

나는 처음 두 방법을 비교했을 때, 첫번째 방식이 직관적이고 이해하기 쉬워서 더 좋은 방식일거라고 생각했다.

그런데, 두번째 방식이 가독성이 좋고 메서드의 재활용성이 올라간다고 한다.
기본적으로 실무에서 이런 부가 메서드들은 잘 보지 않기때문에, 코드가 깔끔한게 더 좋고, 세부 메서드가 궁금해도 타고 들어갈수 있기 때문에 확인도 좋다고 한다..

그리고 가장 최고의 장점은

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

위와 같이 조건문을 묶어서 조합하여 사용가능 하다.

벌크 연산

// 28살 미만의 데이터 이름 바꾸기
long count = queryFactory
          .update(member)
	  .set(member.username, "비회원") 
	  .where(member.age.lt(28)) 
	  .execute();

// 전체 나이 1살 증가
long count = queryFactory
          .update(member)
          .set(member.age, member.age.add(1))
          .execute();

// 18살 초과 데이터 삭제
long count = queryFactory
            .delete(member)
            .where(member.age.gt(18))
            .execute();

.execute() 를 활용한다.
JPQL 배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 안전하다.

profile
I am me

0개의 댓글