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가 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문이다.
검색 조건은 .and(), or()를 메서드 체인으로 연결할 수 있다.
JPQL이 제공하는 모든 검색 조건을 제공한다.
where()에 검색 조건을 추가하면 AND 조건이 추가된다. 이 경우 null 값은 무시되고 이를 활용해서 메서드 추출을 통해 동적 쿼리를 깔끔하게 만들 수 있다.
fetch() : 리스트 반환
fetchOne() : 단 건 반환
fetchFirst() : 첫 번째 값 반환
orderBy()
desc(), asc() : 일반 정렬
nullsLast(), nullsFirst() : null 데이터 순서 부여
카운트 쿼리는 5.0 버전부터 사라졌다. 페이징 쿼리와 따로 카운트 쿼리를 작성하자
offset() : 시작 인덱스
limit() : 조회 건 수
기본 집합 함수와 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))
...
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고 두 번째 파라미터에 별칭으로 사용할 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()이라고 추가하면 된다.
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();
// CaseBuilder를 사용한다
NumberExpression<Integer> rankPath = new CaseBuilder()
.when(member.age.between(0, 20)).then(2)
.when(member.age.between(21, 30)).then(1)
.otherwise(3);
// 상수가 필요하면 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();
프로젝션 : 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);
}
결과를 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();
엔티티가 아닌 객체의 생성자에 @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();
// 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;
}
execute() 메서드를 사용한다. JPQL 배치와 마찬가지로 배치 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화 하자
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute();
JPA와 같이 Dialect에 등록된 내용만 호출 가능하다. 대부분의 ANSI 표준 함수들은 querydsl이 내장하고 있다.
// replace 함수 사용
String result = queryFactory
.select(Expressions.stringTemplate("function('replace', {0}, {1}, {2})",
member.username, "member", "M"))
.from(member)
.fetchFirst();
출처 : 실전! Querydsl