[JPA] QueryDSL
QueryDSL
- JPQL을 생성하는 빌더 API
-
QueryDSL은 보통 동적 쿼리를 위해 사용한다
- 특징
- 쿼리를 자바 코드로 작성해서 에러를 컴파일 시점에 잡을 수 있다
- 문법이 SQL과 비슷해 가독성이 좋다
-
BooleanExpression
,BooleanBuilder
를 통해 동적쿼리를 편하게 작성할 수 있다 -
PreparedStatement
가 동작하여 자동으로 파라미터를 바인딩 해준다
Spring 프로젝트 설정
plugins {
...
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
...
}
...
dependencies {
implementation 'com.querydsl:querydsl-jpa'
}
...
# gradle 맨 밑부분에 QueryDSL task 설정부분도 추가
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
@Configuration
public class QuerydslConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
-
EntityManager
와JPAQueryFactory
를 설정하여 프로젝트 어디서든 사용 가능
쿼리타입(Q) = Q도메인
- QueryDSL을 사용 하려면 쿼리 타입이라는 쿼리용 클래스가 있어야 한다.
-
쿼리 타입은 기본 인스턴스를 보관하고 있다. 하지만, 같은 엔티티를 서브쿼리에 사용하면 같은 별칭이 사용되므로 별칭을 직접 지정해야 한다.
-
gradle -> Tasks -> build -> clean
: 빌드 초기화 -
gradle -> Tasks -> other -> compileQuerydsl
: Q도메인 빌드 - 빌드를 하면
build/generated/querydsl
아래에 Q도메인 객체가 추가된다.
@Entity
@Table(name = "member")
public class MemberEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(nullable = false)
private String name;
}
→ QueryDSL은 MemberEntity 동일한 패키지에 QMemberEntity
라는 이름을 가진 쿼리 타입을 생성한다.
// 기본 인스턴스를 사용하여 쿼리 타입에 접근
QMemberEntity qMember = QMemberEntity.memberEntity;
// 별칭을 직접 지정하여 쿼리 타입에 접근
QMemberEntity qMember = new QMemberEntity("m"); // 생성되는 JPQL의 별칭 = m
문법
SELECT문
public void queryDSL() {
// 1. 직접 생성
// EntityManager em = emf.createEntityManager();
// JPAQuery query = new JPAQuery(em);
// 2. Bean 사용하기
@Autowired
private final JPAQueryFactory queryFactory;
QMemberEntity qMember = QMemberEntity.memberEntity; // 기본 인스턴스 사용
// SELECT qMember FROM QMemberEntity qMember WHERE qMember.name=?1
List<MemberEntity> list = queryFactory
.selectFrom(qMember)
.where(qMember.name.eq("europani"))
.fetch();
return list;
}
결과반환
메서드 | 설명 |
---|---|
List<.T> fetch() | 조회 결과 리스트 반환 |
T fetchFirst() | 조회 결과 첫번째 것만 반환 |
(조회쿼리, count쿼리 총 2번실행) |
→ QueryDSL 5.0부터 fetchCount()
, fetchResults()
deprecated 되어짐
where 조건
검색조건 | 설명 |
---|---|
member.username.eq(“a”) | username = ‘a’ |
member.username.ne(“a”) | username != ‘a’ |
member.username.isNotNull() | username is not null |
member.age.in(10, 20) | age in (10, 20) |
member.age.notIn(10, 20) | age not in (10, 20) |
member.age.between(10, 20) | age between 10 and 20) |
member.age.goe(15) | age >= 15 |
member.age.gt(15) | age > 15 |
member.age.loe(15) | age <= 15 |
member.age.lt(15) | age < 15 |
member.username.like(“member%”) | username like ‘member%’ |
member.username.contains(“member”) | username like ‘%member%’ |
member.username.startWith(“member”) | username like ‘member%’ |
and, or
// and
List<MemberEntity> findMember = queryFactory
.selectFrom(qMember)
.where(qMember.username.eq("europani"), qMember.age.eq(20))
.fetch();
// or
List<MemberEntity> findMember = queryFactory
.selectFrom(qMember)
.where(qMember.username.eq("europani")
.or(qMember.username.eq("acaka")))
.fetch();
정렬
List<MemberEntity> result = queryFactory
.selectFrom(qMember)
.where(qMember.age.eq(100))
.orderBy(qMember.age.desc(), qMember.username.asc().nullsLast())
.fetch();
→ nullsFirst()
, nullsLast()
: null 데이터 순서부여
페이징
List<MemberEntity> result = queryFactory
.selectFrom(qMember)
.where(qMember.age.eq(100))
.orderBy(qMember.age.desc())
.offset(1) // index 1번부터
.limit(2) // 2개
.fetch();
집합
List<Tuple> result = queryFactory
.select(
qMember.count(), //회원수
qMember.age.sum(), //나이 합
qMember.age.avg(), //나이 평균
qMember.age.max(), //최대 나이
qMember.age.min() //최소 나이
)
.from(qMember)
.fetch();
조인
조인 | 설명 |
---|---|
join(), innerJoin() | 내부 조인 |
leftJoin() | Left 외부 조인 |
rightJoin() | Right 외부 조인 |
조인 .fetchJoin() |
해당 조인에 패치조인 적용 |
List<Tuple> result = queryFactory
.select(qMember.usename, qTeam.name)
.from(qMember)
.join(qMember.team, qTeam)
.fetch();
fetch 조인
public List<User> findAllByQuerydsl() {
QMemberEntity qMember = QMemberEntity.memberEntity;
QTeam qTeam = QTeamEntity.teamEntity;
QArticle qArticle = QArticleEntity.articleEntity;
return queryFactory.selectFrom(qMember)
.join(qMember.team, qTeam).fetchJoin()
.join(qMember.article, qArticle).fetchJoin()
.distinct()
.fetch();
}
→ 패치조인 과정에서 중복 데이터가 발생할 수 있기 때문에 distinct
를 추가했다
연관관계 없는 조인
join(테이블).on(조건)
@Setter
public class RateStatsDTO {
private Long eqAreaId;
private Long fieldId;
private Double average;
}
public List<RateStatsDTO> getStats() {
return queryFactory.select(
Projections.bean(RateStatsDTO.class,
equipment.equipArea.id.as("eqAreaId"),
equipment.field.id.as("fieldId"),
rate.workRate.avg().as("average"))
)
.from(rate)
.join(equipment).on(equipment.id.eq(rate.equipmentId)).fetchJoin()
.groupBy(equipment.equipArea.id, equipment.field.id)
.fetch();
}
→ setter를 통해 맵핑하기 때문에 setter
가 반드시 필요하다
Group By, Having
// 팀의 이름과 각 팀의 25세 이상 멤버의 평균 연령을 구해라
List<Tuple> result = queryFactory
.select(qTeam.name, qMember.age.avg())
.from(qMember)
.join(qMember.team, qTeam)
.groupBy(qTeam.name)
.having(qMember.age.gt(25))
.fetch();
Case문
List<String> result = queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
결과 DTO 반환
- 쿼리 결과를 바로 DTO에 담아서 반환할 수 있다
1. DTO 반환
- setter, field, constructor 방식 등
// setter
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
// field
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
// constructor
List<MemberDto> result = queryFactory
.select(Projections.Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
- 필드 명이 다를 땐 맵핑이 바로 안되기 때문에 alias를 사용해야 한다
List<MemberDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
member.age))
.from(member)
.fetch();
2. @QueryProjection
- DTO의 생성자에 사용하면 해당 DTO의 Q도메인을 생성해 맵핑
- 맵핑시 생기는 오류를 컴파일 시점에 찾을 수 있다
- 생성자 방식 : 런타임 시점에 오류발생
@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;
}
}
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
동적쿼리
1. BooleanBuilder
where문에 들어가는 조건들을 넣어주는 컨테이너
- where 조건문을 한눈에 파악하기 어려움
@Override
public List<Academy> findDynamicQuery(String name, String address, String phoneNumber) {
BooleanBuilder builder = new BooleanBuilder(); // BooleanBuilder 생성
if (!ObjectUtils.isEmpty(name)) { // name 조건
builder.and(academy.name.eq(name));
}
if (!ObjectUtils.isEmpty(address)) { // address 조건
builder.and(academy.address.eq(address));
}
if (!ObjectUtils.isEmpty(phoneNumber)) { // phoneNumber 조건
builder.and(academy.phoneNumber.eq(phoneNumber));
}
return queryFactory
.selectFrom(academy)
.where(builder) // 조건들을 넣은 builder 주입
.fetch();
}
2. BooleanExpression
where문의 조건을 표현하는 표현식
- where 조건문을 표현식으로 리팩토링하여 사용하기 때문에 가독성이 좋다
- 또한, 파라미터가 null인 조건은 생략되게 만들수 있다
@Override
public List<Academy> findDynamicQuery(String name, String address, String phoneNumber) {
return queryFactory
.selectFrom(academy)
.where(eqName(name), // expression들 주입
eqAddress(address),
eqPhoneNumber(phoneNumber))
.fetch();
}
private BooleanExpression eqName(String name) { // name 조건
if (ObjectUtils.isEmpty(name)) {
return null;
}
return academy.name.eq(name);
}
private BooleanExpression eqAddress(String address) { // address 조건
if (ObjectUtils.isEmpty(address)) {
return null;
}
return academy.address.eq(address);
}
private BooleanExpression eqPhoneNumber(String phoneNumber) { // phoneNumber 조건
if (ObjectUtils.isEmpty(phoneNumber)) {
return null;
}
return academy.phoneNumber.eq(phoneNumber);
}
Spring Data JPA와 QueryDSL 함께 사용하기
-
QuerydslRepository
를 사용하여 QueryDSL의 역할을 정의 -
Repository
에JpaRepository
와QuerydslRepository
를 같이 상속시키면 JPA와 QueryDSL을 모두 사용 할 수 있다
(1) QueryDslRepositorySupport 사용
1. Repository 인터페이스
public interface MemberRepository extends JpaRepository<MemberEntity, Integer>, MemberQuerydslRepository {
}
2. Querydsl Repository 인터페이스
- QueryDSL로 만들어 쓸 메서드의 인터페이스
public interface MemberQuerydslRepository {
List<MemberEntity> findByName(String name);
}
3. Querydsl Repository 구현체
-
QuerydslRepositorySupport
상속 - QuerydslRepository 인터페이스 구현
@Repository
public class MemberQuerydslRepositoryImpl extends QuerydslRepositorySupport implements MemberQuerydslRepository {
QMemberEntity member = QMemberEntity.memberEntity;
public MemberQuerydslRepositoryImpl() {
super(MemberEntity.class);
}
public List<MemberEntity> findByName(String name) {
QMemberDto projection = new QMemberDto(member.username, member.age);
return getQuerydsl()
.createQuery()
.select(projection)
.from(member)
.where(member.name.eq(name))
.fetch();
}
}
(2) JPAQueryFactory 사용
QueryDSL은 JPAQueryFactory
를 통해 동작한다
JPAQueryFactory
를 사용하면 EntityManager
를 통해 질의가 처리되고 JPQL을 사용한다
1. Repository 인터페이스
- 인터페이스에서 JPA뿐만 아니라 QueryDSL을 위해 만든 QuerydslRepository 인터페이스까지 상속 받는다
public interface MemberRepository extends JpaRepository<MemberEntity, Integer>, MemberQuerydslRepository {
}
2. Querydsl Repository 인터페이스
- QueryDSL로 만들어 쓸 메서드의 인터페이스
public interface MemberQuerydslRepository {
List<MemberEntity> findByName(String name);
}
3. Querydsl Repository 구현체
- Querydsl Repository 인터페이스를 구현
@RequiredArgsConstructor
public class MemberQuerydslRepositoryImpl implements MemberQuerydslRepository {
private final JPAQueryFactory queryFactory;
QMemberEntity member = QMemberEntity.memberEntity;
@Override
public List<MemberEntity> findByName(String name) {
return queryFactory.selectFrom(member)
.where(member.name.eq(name))
.fetch();
}
}
페이징
- Spring Data JPA의
Page
,Pageable
객체를 활용 할수 있다 -
(deprecated)fetchResults()
를 통해 조회쿼리, count쿼리 총 2번의 쿼리실행
public Page<MemberEntity> findByName(String name, Pageable pageable) {
QueryResults<MemberEntity> results = queryFactory.selectFrom(member)
.where(member.name.eq(name))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
List<MemberEntity> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total);
}
→ PageImpl
: Page
구현체
- 최적화를 위한 count쿼리 분리
public Page<MemberEntity> findByName(String name, Pageable pageable) {
List<MemberEntity> content = queryFactory.selectFrom(member)
.where(member.name.eq(name))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long total = queryFactory.select(member.id.count())
.from(member)
.where(member.name.eq(name))
.fetchFirst();
return new PageImpl<>(content, pageable, total);
}
-
PageableExecutionUtils.getPage(content, pageable, function)
- count쿼리 생략을 통해 최적화 가능
-
content
와pageable
사이즈를 확인 후 생략 가능한 경우 function을 실행하지 않음
1) 처음 페이지만 존재하는 경우 (데이터 갯수 < 페이지 사이즈이면 total = 데이터갯수)
2) 마지막 페이지일 경우 (total = offset + 현재 페이지 데이터갯수)
public Page<MemberEntity> findByName(String name, Pageable pageable) {
List<MemberEntity> content = queryFactory.selectFrom(member)
.where(member.name.eq(name))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
JPAQuery<MemberEntity> countQuery = queryFactory.select(member.id.count())
.from(member)
.where(member.name.eq(name));
return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchFirst());
}
-
QuerydslRepositorySupport
을 사용하면applyPagination()
로 적용 가능
public <T> JPQLQuery<T> applyPagination(Pageable pageable, JPQLQuery<T> query) {
...
query.offset(pageable.getOffset());
query.limit(pageable.getPageSize());
return applySorting(pageable.getSort(), query);
}
public Page<MemberEntity> findByName(String name, Pageable pageable) {
JPAQuery<MemberEntity> query = getQuerydsl()
.createQuery()
.select()
.from(member)
.where(member.name.eq(name))
.fetch();
JPAQuery<MemberEntity> pageQuery = getQuerydsl()
.applyPagination(pageable, query);
JPAQuery<MemberEntity> countQuery = getQuerydsl()
.createQuery()
.select(member.id.count())
.from(member)
.where(member.name.eq(name));
return PageableExecutionUtils.getPage(pageQuery.fetch(), pageable, () -> countQuery.fetchFirst());
}