[복습] Querydsl - 중급 문법

김주형·2024년 6월 25일
0

Web Basic

목록 보기
49/57
post-thumbnail

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

프로젝션이란?
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로 조회합니다

실행 결과

튜플 조회

프로젝션 대상이 둘 이상일 때 사용합니다
com.querydsl.core.Tuple

@Test
public void tupleProjection() {
List 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.username);
System.out.println("username = " + username);
System.out.println("age = " + age);
}
}


- Tuple은 리포지토리 계층 안에서 쓰는 정도는 괜찮지만, 그 밖의 계층에서는 DTO로 변환하는 것이 좋습니다

실행 결과
![](https://velog.velcdn.com/images/urtimeislimited/post/aec15708-4ea2-448f-ae6d-6fa4b091e789/image.png)



## 프로젝션과 결과 반환 - DTO 조회 

### 순수 JPA에서 DTO 조회
MemberDto
```java
package study.querydsl.dto;  

import lombok.Data;  
import lombok.NoArgsConstructor;  

@Data  
@NoArgsConstructor  
public class MemberDto {  

  private String username;  
  private int age;  

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

순수 JPA에서 DTO 조회 코드

  @Test  
  public 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);  
  }  
  }  
  • 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야 합니다
  • DTO의 package 이름을 다 적어줘야 해서 지저분합니다
  • 생성자 방식만 지원합니다

실행 결과

Querydsl 빈 생성(Bean population)

결과를 DTO 반환할 때 사용하며, 3가지 방법을 지원합니다

  • 프로퍼티 접근
  • 필드 직접 접근
  • 생성자 사용
  1. 프로퍼티 접근 - Setter
@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);
 	}
  • Projections.bean(주입대상클래스, 프로퍼티1, 프로퍼티2) 방식으로 값을 주입합니다

실행 결과

  1. 필드 직접 접근
@Test  
public 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);  
    }  
}
  • Getter, Setter가 없어도 필드에 바로 값을 주입합니다
  • Projections.fields(주입 대상 클래스, 필드1, 필드2)

실행 결과

별칭이 다를 때
UserDto

package study.querydsl.dto;  
  
import lombok.Data;  
  
@Data  
public class UserDto {  
  
    private String name;  
    private int age;  
  
  
    public UserDto() {  
    }  
  
    public UserDto(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  
}
@Test  
public void findUserDto() {  
    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.as(source, alias): 필드나, 서브 쿼리에 별칭을 적용합니다
    username.as("memberName"): 필드에 별칭 적용합니다

실행 결과

  1. 생성자 사용
@Test  
public 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);  
    }  
}

실행 결과


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

생성자 + @QueryProjection

package study.querydsl.dto;  
  
import com.querydsl.core.annotations.QueryProjection;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
  
@Data  
@NoArgsConstructor  
public class MemberDto {  
  
    private String username;  
    private int age;  
  
    @QueryProjection  
    public MemberDto(String username, int age) {  
        this.username = username;  
        this.age = age;  
    }  
  
}
  • DTO 프로젝션에 사용될 생성자에 @QueryProjection을 붙여줍니다
  • 이를 통해 DTO로 Q타입을 생성해줍니다

@QueryProject 추가 후, compileQuerydsl 클릭

QMemberDto 생성되었습니다


QueryProjection 활용

@Test  
public 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);  
    }  
}
  • 이 방법은 컴파일러로 타입을 체크할 수 있으므로 가정 안전한 방법입니다
  • DTO에 QueryDSL 어노테이션을 유지해야 하고 DTO까지 Q파일을 생성해야 하는 단점이 있습니다

실행 결과

distinct 사용

List<String> result = queryFactory
        .select(member.username).distinct()
        .from(member)
        .fetch();
  • JPQL의 distinct와 같습니다

동적 쿼리 - BooleanBuilder 사용

동적 쿼리를 해결하는 두 가지 방식

  • BooleanBuilder
  • Where 다중 파라미터 사용

BooleanBuilder 사용

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

BooleanBuilder를 통한 동적 쿼리 생성은 빌더를 생성 후 필요한 조건을 null 확인 여부에 따라 and 또는 or 등으로 더해주면 됩니다

실행 결과


동적 쿼리 - Where 다중 파라미터 사용

@Test
public void dynamicQuery_WhereParam() throws Exception {
  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(usernameEq(usernameCond), ageEq(ageCond))
          //   .where(allEq(usernameCond, 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;
    }
  }
  • where 조건에 null 값은 무시됩니다
  • usernameEq()와 같은 메서드를 다른 쿼리에서도 재활용할 수도 있습니다
  • 쿼리 자체의 가독성이 높아집니다

실행 결과

메서드 조합 가능

private BooleanExpression allEq(String usernameCond, Integer ageCond) {  
    return usernameEq(usernameCond).and(ageEq(ageCond));  
}
  • method chaining 가능
  • null 체크는 주의해서 처리해야 합니다

수정, 삭제 벌크 연산

쿼리 한번으로 대량 데이터 수정

@Test
@Commit
public void bulkUpdate() {
    // member1 = 10 -> DB member1
    // member2 = 20 -> DB member2
    // member3 = 30 -> DB member3
  	long count = queryFactory
  				.update(member)
   				.set(member.username, "비회원")
  				.where(member.age.lt(28))
  				.execute(); // count: 용량을 받은 회원 수 
  
    // member1 = 10 -> DB 비회원
    // member2 = 20 -> DB 비회원
    // member3 = 30 -> DB member3
    // member4 = 40 -> DB member4
    List<Member> result = queryFactory
  				.selectFrom(member)
  				.fetch();
    for (Member member1: result) {
  		System.out.println("member1 = " + member1);
    }
 }
  • db상태와 영속성 컨텍스트 상태가 달라집니다
  • db → 영속성 컨텍스트에 데이터를 반영할 때, 영속성 컨텍스트에 데이터가 존재한다면 db 데이터는 버립니다 (우선순위 : 영속성 컨텍스트, db)
  • 벌크연산을 실행하고 나면(영속성 컨텍스트에서 db로 데이터를 다 보냈다면), 영속성 컨텍스트 초기화 하기! (em.flush(), em.clear())

실행 결과
db

소스 실행

기존 숫자에 1 더하기

@Test
public void bulkAdd() {
  	long count = queryFactory
  				.update(member)
  			    .set(member.age, member.age(add(1))
  				.execute();
 }
  • 곱하기는 multiply(x)

실행 결과

쿼리 한번으로 대량 데이터 삭제

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

실행 결과

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


SQL function 호출하기

SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있습니다

member -> M으로 변경하는 replace 함수 사용

@Test  
public void sqlFunction() {  
    List<String> result = queryFactory  
            .select(Expressions.stringTemplate(  
                    "function('replace', {0}, {1}, {2})",  
                    member.username, "member", "M"))  
            .from(member)  
            .fetch();  
  
    for (String s : result) {  
        System.out.println("s = " + s);  
    }  
}
  • member가 M으로 변경됩니다

실행 결과

lower 함수 - 소문자로 변경해서 비교

@Test  
    public void sqlFunction2() {  
        List<String> result = queryFactory  
                .select(member.username)  
                .from(member)  
                .where(member.username.eq(  
                        Expressions.stringTemplate("function('lower', {0})", member.username)))  
//                .where(member.username.eq(member.username.lower()))  
                .fetch();  
  
        for (String s : result) {  
            System.out.println("s = " + s);  
        }  
    }

lower 같은 ansi 표준 함수들은 querydsl이 대부분 내장하고 있기 때문에 다음과 같이 처리해도 결과는 같습니다

.where(member.username.eq(member.username.lower()))
실행 결과

profile
도광양회

0개의 댓글