Querydsl 문법 정리

김혁준·2024년 7월 7일

JPA

목록 보기
15/16

0.Querydsl

Querydsl은 복잡한 쿼리, 동적 쿼리를 자바 코드로 편리하게 작성하도록 하는 라이브러리이다.
JPA와 같이 활용하면 찰떡궁합이다.
EntityManager로 JPAQueryFactory를 생성하고 사용한다. 즉, Querydsl은 JPQL 빌더이다.
순수 JPQL과 차이점은 컴파일 시점에 오류를 검증하고 파라미터 바인딩을 자동으로 처리한다는 것이다.

@SpringBootTest
@Transactional
public class QuerydslBasicTest {
    @PersistenceContext
    EntityManager em;
    
    JPAQueryFactory queryFactory;
    
    @BeforeEach
    public void before() {
        queryFactory = new JPAQueryFactory(em);
	}
    
    @Test
    public void startQuerydsl2() {
		QMember m = new QMember("m");
        Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq("member1"))
                .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
}

JPAQueryFactory를 필드로 제공해도 동시성 문제는 발생하지 않는다. JPAQueryFactory를 생성하는 EntityManager가 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문이다.

1. 기본 문법

1-0 기본적으로 JPQL과 비슷하다. 필요한 문법은 그때그때 찾아보자.

1-1 검색 조건 쿼리

검색 조건은 .and(), or()를 메서드 체인으로 연결할 수 있다.
JPQL이 제공하는 모든 검색 조건을 제공한다.
where()에 검색 조건을 추가하면 AND 조건이 추가된다. 이 경우 null 값은 무시되고 이를 활용해서 메서드 추출을 통해 동적 쿼리를 깔끔하게 만들 수 있다.

1-2 결과 조회

fetch() : 리스트 반환
fetchOne() : 단 건 반환
fetchFirst() : 첫 번째 값 반환

1-3 정렬

orderBy()
desc(), asc() : 일반 정렬
nullsLast(), nullsFirst() : null 데이터 순서 부여

1-4 페이징

카운트 쿼리는 5.0 버전부터 사라졌다. 페이징 쿼리와 따로 카운트 쿼리를 작성하자

offset() : 시작 인덱스
limit() : 조회 건 수

1-5 집합

기본 집합 함수와 groupBy, having 메서드가 제공된다.

@Test
public void aggregation() throws Exception {
    List<Tuple> result = queryFactory
            .select(member.count(),
                    member.age.sum(),
                    member.age.avg(),
                    member.age.max(),
                    member.age.min())
            .from(member)
            .fetch();
    Tuple tuple = result.get(0);
    assertThat(tuple.get(member.count())).isEqualTo(4);
    assertThat(tuple.get(member.age.sum())).isEqualTo(100);
    assertThat(tuple.get(member.age.avg())).isEqualTo(25);
    assertThat(tuple.get(member.age.max())).isEqualTo(40);
    assertThat(tuple.get(member.age.min())).isEqualTo(10);
}

...
     .groupBy(item.price)
     .having(item.price.gt(1000))
...

1-6 조인

조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고 두 번째 파라미터에 별칭으로 사용할 Q타입을 지정한다.
-> join(조인 대상, 별칭으로 사용할 Q타입)
join(), innerJoin() : 내부조인
leftJoin() : left 외부 조인
rightJoin() : right 외부 조인
세타 조인 : from 절에 여러 엔티티를 선택해서 세타 조인. 외부 조인은 on절을 사용하자

조인 - on절 : 조인 대상 필터링, 연관관계 없는 외부 조인

// 내부조인이면 where 절로 해결하고 외부조인이 필요한 경우에만 사용하자
List<Tuple> result = queryFactory
             .select(member, team)
             .from(member)
             .leftJoin(member.team, team).on(team.name.eq("teamA"))
             .fetch();

// 회원의 이름과 팀의 이름이 같은 대상 외부 조인
// on조인의 경우에는 leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
List<Tuple> result = queryFactory
             .select(member, team)
             .from(member)
             .leftJoin(team).on(member.username.eq(team.name))
             .fetch();

조인 - 페치 조인 : join(), leftJoin()등 조인 기능 뒤에 fetchJoin()이라고 추가하면 된다.

1-7 서브 쿼리

JPA의 JPQL 서브쿼리의 한계로 인해 from 절의 서브쿼리는 지원하지 않는다. 당연히 Querydsl도 지원하지 않는다.
from 절에 서브쿼리를 적어야 한다면 서브쿼리를 join으로 변경하거나 쿼리를 분리하거나 네이티브 SQL을 사용한다.

QMember memberSub = new QMember("memberSub");

// JPAExpressions를 사용한다.
List<Member> result = queryFactory
             .selectFrom(member)
             .where(member.age.goe(
                     JPAExpressions
                             .select(memberSub.age.avg())
                             .from(memberSub)
)) .fetch();

1-8 Case 문법

// CaseBuilder를 사용한다
NumberExpression<Integer> rankPath = new CaseBuilder()
         .when(member.age.between(0, 20)).then(2)
         .when(member.age.between(21, 30)).then(1)
         .otherwise(3);

1-9 상수, 문자 더하기

// 상수가 필요하면 Expressions.constant()를 사용
Tuple result = queryFactory
         .select(member.username, Expressions.constant("A"))
         .from(member)
         .fetchFirst();

// 문자 더하기는 concat을 사용한다. 문자가 아닌 다른 타입들을 stringValue()로 문자로 변환한다.
String result = queryFactory   						   .select(member.username.concat("_").concat(member.age.stringValue()))
         .from(member)
         .where(member.username.eq("member1"))
         .fetchOne();

2. 중급 문법

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

프로젝션 : select 대상 지정

프로젝션 대상이 하나

// 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다. 둘 이상이면 튜플이나 DTO로 조회
List<String> result = queryFactory
	.select(member.username)
     .from(member)
     .fetch();
     
// 프로젝션 대상이 둘 이상일때 튜플로 조회
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);
     }

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

결과를 DTO로 반환할 때 사용한다.
프로퍼티 접근, 필드 직접 접근, 생성자 사용의 방법으로 조회한다.

// 프로퍼티 접근 - 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();
 
// 생성자 사용
List<MemberDto> result = queryFactory
         .select(Projections.constructor(MemberDto.class,
                 member.username,
                 member.age))
         .from(member)
.fetch(); 
}

// 프로퍼티나 필드 접근 방식에서 이름이 다를 때 .as() 메서드로 별칭을 적용한다.
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();

2-3 @QueryProjection

엔티티가 아닌 객체의 생성자에 @QueryProjection을 적으면 Q 객체가 생성된다.

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

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

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

2-4 동적 쿼리 - BooleanBuilder, Where 다중 파라미터

// BooleanBuilder() 를 활용
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();
}

// BooleanExpression을 활용해서 조건 추가
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;
     }

2-5 벌크 연산

execute() 메서드를 사용한다. JPQL 배치와 마찬가지로 배치 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화 하자

 long count = queryFactory
         .update(member)
		 .set(member.username, "비회원") 
         .where(member.age.lt(28))
         .execute();

2-6 SQL function

JPA와 같이 Dialect에 등록된 내용만 호출 가능하다. 대부분의 ANSI 표준 함수들은 querydsl이 내장하고 있다.

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

출처 : 실전! Querydsl

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard

0개의 댓글