프로젝션과 결과 반환, 동적쿼리, 벌크 쿼리

Gyeongjae Ham·2023년 6월 18일
0

QueryDSL

목록 보기
2/5
post-thumbnail

해당 시리즈는 김영한님의 JPA 로드맵을 따라 학습하면서 내용을 정리하는 글입니다

프로젝션 결과 반환

  • 프로젝션: SELECT 대상 지정

프로젝션 대상이 하나

List<String> result = queryFactory
						.select(member.username)
                        .from(member)
                        .fetch();
  • 프로젝션 대상이 하나면 명확하게 타입을 지정할 수 있습니다
  • 대상이 둘 이상일 경우에는 튜플이나 DTO로 조회를 합니다
    • 클라이언트에게 내부 구조를 드러낼 필요가 없고, 라이브러리의 버전 업데이트 등 사용성이 변화할 가능성이 있기 때문에 DTO를 사용하는 것을 지향합니다

튜플 조회

  • com.querydsl.core.Tuple
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 조회

  • 순수 JPA에서 DTO 조회

MemberDto

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

순수 JPA에서 DTO 조회

List<MemberDto> result = em.createQuery(
				"SELECT new study.querydsl.dto.MemberDto(m.username, n.age) "
                + "from Member m", MemberDto.class)
                .getResultList();
  • 순수 JPA에서 DTO로 조회할 때는 new 명령어를 사용해야 합니다
  • DTOpackage 이름을 다 적어줘야해서 지저분한 감이 있습니다
  • 생성자 방식만 지원합니다

QueryDSL 빈 생성(Bean population)

  • 결과를 DTO로 반환할 때 사용(3가지 방법)
    1. 프로퍼티 접근
    2. 필드 직접 접근
    3. 생성자 사용

프로퍼티 접근 - Setter

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

필드 직접 접근

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

별칭이 다를 경우

@Data
public class UserDto {
	private String name;
    private int age;
}
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") : 필드에 별칭 적용

생성자 사용

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

@QueryProjection

생성자 + @QueryProjection

@Data
public class MemberDto {
	private String username;
    private int age;
    
    public MemberDto() {}
    
    @QueryProjection
    public MemberDto(String username, int age) {
    	this.username = username;
        this.age = age;
    }
}
  • QMemberDto가 생성됩니다
List<MemberDto> result = queryFactory
						.select(new QMemberDto(member.username, member.age))
                        .from(member)
                        .fetch();
  • 이 방법은 컴파일러로 타입을 확인할 수 있으므로 가장 안전한 방법입니다
  • 다만 DTOQueryDSL 어노테이션을 유지해야 하는 점과 DTO까지 Q클래스 파일을 생성해야 하는 단점이 있습니다

동적 쿼리 - BooleanBuilder

  • 동적 쿼리를 해결하는 두 가지 방식
    1. BooleanBuilder
    2. WHERE 다중 파라미터 사용

BooleanBuilder

@Test
public void 동적쿼리_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();

WHERE 다중 파라미터 사용

  • 코드가 깔끔해지기 때문에 더 선호되는 방법입니다
@Test
public void 동적쿼리_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))
            .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 값은 무시됩니다
  • 메서드를 다른 쿼리에서도 재활용 할 수 있습니다
  • 쿼리 자체의 가독성이 높아집니다

수정, 삭제 벌크 연산

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

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

기존 숫자에 2 곱하기

long count = queryFactory
				.update(member)
                .set(member.age, member.age.multiply(2))
                .execute();

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

long count = queryFactory
				.delete(member)
                .where(member.age.gt(18))
                .execute();

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

SQL function 호출하기

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

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

String result = queryFactory
				.select(Expressions.stringTemplate(
                "function('replace', {0}, {1}, {2})",
                member.username, "member", "M"))
                .from(member)
                .fetchFirst();

소문자로 변경해서 비교하기

.select(member.username)
.from(member)
.where(member.username.eq(
Expressions.stringTemplate("function('lower', {0})", member.username)))

-> lower같은 ansi 표준 함수들은 QueryDSL이 상당부분 내장하고 있습니다. 따라서 아래와 같이 처리해도 결과가 같습니다

.where(member.username.eq(member.username.lower()))
profile
Always be happy 😀

0개의 댓글