Java JPA의 한계를 넘어서 쿼리를 자바 코드로 작성.(Creiteria는 복잡함에 의해 실용성이 떨어짐)
문법 오류를 컴파일 시점에 확인해줌
동적 쿼리문제를 쉽게 해결 해줌.
익숙하고 쉬운 SQL 문법 형식
스프링부트3 버전 이후
// Querydsl 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl apt: ${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
스프링부트3 버전 이전
ext["hibernate.version"] = "5.6.5.Final"
dependencies {
//Querydsl 추가
implementation 'com.querydsl:querydsl-jpa'
annotationProcessor "com.querydsl:querydsl apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
공통
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}
Gradle IntelliJ 사용법
Gradle - Tasks - build - clean
Gradle - Tasks - other - compileQuerydsl
Gradle 콘솔 사용법
./gradlew clean compileQuerydsl
Q 타입 생성 확인
빌드 후, build - generated 안에 QFile이 생겨야 됨. // (git에 올리면 안된다)
Querydsl vs JPQL , 서로의 쿼리문을 비교해보자
@Autowired EntityManager em;
@Test
public void startJPQL() {
//member1을 찾아라.
String qlString = "select m from Member m " +
"where m.username = :username";
Member findMember = em.createQuery(qlString, Member.class)
.setParameter("username", "member1")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
@Test
public void startQuerydsl() {
//member1을 찾아라.
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember m = new QMember("m");
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1"))//파라미터 바인딩 처리
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
Member findMember = queryFactory
.select(QMember.member)
.from(QMember.member)
.where(QMember.member.username.eq("member1"))//파라미터 바인딩 처리
.fetchOne();
QMember를 static import를 하면 위의 것 보다 더 줄일 수 있다.
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1"))//파라미터 바인딩 처리
.fetchOne();
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1")
.and(member.age.eq(10)))
.fetchOne();
JPQL이 제공하는 모든 검색 조건 제공
member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'
member.username.isNotNull() //이름이 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,30) //between 10, 30
member.age.goe(30) // age >= 30
member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30
member.age.lt(30) // age < 30
member.username.like("member%") //like 검색
member.username.contains("member") // like ‘%member%’ 검색
member.username.startsWith("member") //like ‘member%’ 검색
AND 조건을 파라미터로 처리
.where(member.username.eq("member1"), member.age.eq(10))
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
//단 건
Member findMember1 = queryFactory
.selectFrom(member)
.fetchOne();
//처음 한 건 조회
Member findMember2 = queryFactory
.selectFrom(member)
.fetchFirst();
// fetchResults() ,fetchCount()는 Deprecated(향후 미지원)
//페이징에서 사용
QueryResults<Member> results = queryFactory
.selectFrom(member)
.fetchResults();
//count 쿼리로 변경
long count = queryFactory
.selectFrom(member)
.fetchCount();
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) //0부터 시작(zero index)
.limit(2) //최대 2건 조회
.fetch();
List<Tuple> result = queryFactory
.select(member.count(),
member.age.sum(),
member.age.avg(),
member.age.max(),
member.age.min()
)
.from(member)
.fetch();
그룹화
List<Tuple> result = queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
f.fetch();
기본 조인
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.
join(조인 대상, 별칭으로 사용할 Q타입)
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
on절
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
* JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'teamA'
* SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='teamA'
*/
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(member.team, team).on(team.name.eq("teamA"))
.fetch();
/**
*
* 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
* JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
* SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name
*/
@Test
public void join_on_no_relation() throws Exception {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(team).on(member.username.eq(team.name))
.fetch();
}
}
QueryDsl에서 페치조인은 어떻게 하는가?
페치 조인은 SQL에서 제공하는 기능은 아니다. SQL조인을 활용해서 연관된 엔티티를 SQL 한번에 조회하는 기능이다. 주로 성능 최적화에 사용하는 방법이다
Member findMember = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
public void subQuery() throws Exception {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub)
))
.fetch();
from 절의 서브쿼리 한계
JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl도 지원하지 않는다.
하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.
from 절의 서브쿼리 해결방안
1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
3. nativeSQL을 사용한다.
단순한 조건
List<String> result = queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타")
)
.from(member)
.fetch();
복잡한 조건
List<String> result = queryFactory
.select(new CaseBuilder()
.when(member.age.between(0, 20)).then("0~20살")
.when(member.age.between(21, 30)).then("21~30살")
.otherwise("기타")
)
.from(member)
.fetch();
orderBy에서 Case 문 함께 사용하기 예제
NumberExpression<Integer> rankPath = new CaseBuilder()
.when(member.age.between(0, 20)).then(2)
.when(member.age.between(21, 30)).then(1)
.otherwise(3);
List<Tuple> result = queryFactory
.select(member.username, member.age, rankPath)
.from(member)
.orderBy(rankPath.desc())
.fetch();
상수가 필요하면 Expressions.constant(xxx) 사용
Tuple result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetchFirst();
문자 더하기 concat
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
기본
프로젝션: select 대상 지정
프로젝션 대상이 하나
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
튜플 조회
프로젝션 대상이 둘 이상일 때 사용
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 조회
Querydsl 빈 생성(Bean population)
결과를 DTO 반환할 때 사용
다음 3가지 방법 지원
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();
별칭이 다를 때
package study.querydsl.dto;
import lombok.Data;
@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();
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username, member.age))
.from(member)
.fetch();
}
가장 깔금한 해결책이긴 한데 단점이 있다.
@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();
distinct
List<String> result = queryFactory
.select(member.username).distinct()
.from(member)
.fetch();
Querlydsl로 동적쿼리를 작성하는 2가지 방법
List<Member> result = searchMember1(usernameParam, ageParam);
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();
}
List<Member> result = searchMember2(usernameParam, ageParam);
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;
}
private Predicate ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null;
}
private BooleanExpression allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
JPA의 dirty checking은 건건이 업데이트문이 날아간다. 이런 것을 벌크 연산으로 한번에 실행 가능.
쿼리 한번으로 대량 데이터 수정
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();
update member
set age = age + 1
쿼리 한번으로 대량 데이터 삭제
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
주의: JPQL 배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 안전하다.
SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있다
queryFactory
.selectFrom(Expressions.stringTemplate(
"function('replace', {0}, {1}, {2})", member.username, "member","M"))
.from(Mmember)
.fetchFrist()
소문자로 변경해서 비교해라.
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();
lower 같은 ANSI 표준 함수들은 querydsl이 상당부분 내장하고 있다. 따라서 다음과 같이 처리해도 결과는 같다.
@Repository
public interface Repository {
private final EntityManager em;
private final JPAQueryFactory queryFactory;
public Repository(EntityManager em) {
this.em = em;
this.queryFactory = new JPAQueryFactory(em);
}
}
@RequiredArgsConstructor
는 사용못하긴 하지만, 직접 생성자로 넣어주면 됨. @Configuration
public class QueryDslConfig {
@PersistenceContext
public EntityManager em;
@Bean
public JPAQueryFactory queryFactory() {
return new JPAQueryFactory(em);
}
}
@Repository
@RequiredArgsConstructor
public interface Repository {
private final JPAQueryFactory queryFactory;
}
참고: 동시성 문제는 걱정하지 않아도 된다. 왜냐하면 여기서 스프링이 주입해주는 엔티티 매니저는 실제 동작 시점에 진짜 엔티티 매니저를 찾아주는 프록시용 가짜 엔티티 매니저이다. 이 가짜 엔티티 매니저는 실제 사용 시점에 트랜잭션 단위로 실제 엔티티 매니저(영속성 컨텍스트)를 할당해준다.
실무에서 동적 쿼리를 짤때는 기본 조건이라도 있는것이 좋다. 만약 조건 쿼리가 없어서 전부 가져온다면 순식간에 몇십만, 몇백만건의 데이터를 퍼올릴 수 있음. (큰일난다는 뜻)
따라서, limit라던가, 페이징이라던가 무슨 조건이라도 있는 것이 좋음.
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetch();
}
private BooleanExpression usernameEq(String username) {
return hasText(username) ? null : member.username.eq(username);
}
private BooleanExpression teamNameEq(String teamName) {
return hasText(teamName) ? null : team.name.eq(teamName);
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
RestAPI로..
@GetMapping("/members")
public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
return memberJpaRepository.search(condition);
}
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username);
}
사용자 정의 리포지토리 만드는 법
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
public class MemberRepositoryImpl implements MemberRepositoryCustom
private final JPAQueryFactory queryFactory;
@Override
List<MemberTeamDto> search(MemberSearchCondition condition) {
...
}
}
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> content = queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
long total = queryFactory
.select(member)
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetchCount();
//return new PageImpl<>(content, pageable, total); 밑의 방식으로 리턴
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);
QuerydslPredicateExecutor 인터페이스
public interface QuerydslPredicateExecutor<T> {
Optional<T> findById(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
long count(Predicate predicate);
boolean exists(Predicate predicate);
// … more functionality omitted.
}
리포지토리에 적용
interface MemberRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
테스트
Iterable result = memberRepository.findAll(
member.age.between(10, 40).and(member.username.eq("member1"))
);
한계점