JPQL과 QueryDSL
- Querydsl은 JPQL 빌더 역할을 한다.
- 차이점
- JPQL
- 문자열로 작성하기 때문에 실행 시점에 오류를 찾아낸다.
- 직접 파라미터 바인딩을 해줘야 한다.
- Querydsl
- 코드로 작성하기 때문에 컴파일 시점 오류를 찾아낸다.
- 자동으로 파라미터를 바인딩 해준다.
- JPQL
맛보기
JPQL
String qlString = "select m from Member m where m.username = :username";
Member findMember =
em.createQuery(qlString, Member.class)
.setParameter("username", "member1")
.getSingleResult();
QueryDSL
EntityManager
로JPAQueryFactory
를 생성한다.
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember m = new QMember("m");
Member findMember =
queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1")) //파라미터 바인딩 처리
.fetchOne();
JPAQueryFactory를 공통 필드로 사용하기
- JPAQueryFactory를 필드로 제공할 때 동시성 문제는 걱정하지 않아도 된다.
- JPAQueryFactory를 생성할 때 제공하는 EntityManager가 해결해준다.
- 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공한다.
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
public void before() {
queryFactory = new JPAQueryFactory(em);
//...
}
Q-Type 클래스 인스턴스를 사용하는 방법
- 기본적으로는 인스턴스 방식을 사용한다.
- 별칭은 서브 쿼리를 작성할 때 주로 사용된다.
QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
- 기본 인스턴스를 static import해서 사용할 수도 있다.
import static study.querydsl.entity.QMember.member;
기본 검색
- 실제 SQL을 작성하듯이 작성한다.
List<Member> result =
queryFactory
.select(member)
.From(member)
.fetch();
- select 메소드와 from 메소드에서 사용되는 Q-Type을 경우에는 selectFrom 메소드를 사용할 수 있다.
List<Member> result =
queryFactory
.selectFrom(member)
.fetch();
조건 검색
where(Predicate... o)
메소드를 활용해서 조회 조건을 추가할 수 있다.Predicate
클래스를 통해서 조건을 나타낸다.- 만약에 검색 조건 메소드에 파라미터로 들어간 값이 null이라면 해당 조건은 무시된다.
- 예시 : MyBatis
Member findMember =
queryFactory
.selectFrom(member)
.where(
member.username.eq("member1")
,member.age.eq(10)
)
.fetchOne();
select
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
member m1_0
where
m1_0.username=?
and m1_0.age=?
and()
나or()
를 통해서 체인을 걸 수 도 있다.Predicate
를 쉼표로 구분하면 각 조건문끼리는 AND로 연결된다.
Member findMember =
queryFactory
.selectFrom(member)
.where(
member.username.eq("member1").and(member.age.eq(10))
)
.fetchOne();
select
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
member m1_0
where
m1_0.username=?
and m1_0.age=?
WHERE A AND (B OR C)
같은 복합 조건도 사용할 수 있다.
Member findMember =
queryFactory
.selectFrom(member)
.where(
member.username.eq("member1")
,member.age.eq(10).or(member.age.eq(20))
)
.fetchOne();
select
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
member m1_0
where
m1_0.username=?
and (
m1_0.age=?
or m1_0.age=?
)
검색 조건 메소드
역할 | 메소드명 | 사용 예시 | SQL |
---|---|---|---|
같은지 비교 | eq | member.username.eq(“member1”) | username = ‘member1’ |
같지 않은지 비교 | ne | member.username.ne(“member1”) | username != ‘member1’ |
부정 연산 | not | member.username.eq(“member1”).not() | username != ‘member1’ |
NOT NULL 체크 | isNotNull | member.username.isNotNull() | username is not null |
포함 여부 확인 | in | member.age.in(10, 20) | age in (10,20) |
미포함 여부 확인 | notIn | member.age.notIn(10, 20) | age not in (10, 20) |
범위 검색 | between | member.age.between(10, 30) | between 10, 30 |
XX 이상 | goe | member.age.goe(30) | age >= 30 |
XX 초과 | gt | member.age.gt(30) | age > 30 |
XX 이하 | loe | member.age.loe(30) | age <= 30 |
XX 미만 | lt | member.age.lt(30) | age < 30 |
패턴에 의한 부분 일치 검색 | like | member.username.like(“member%”) | username like ‘member%’ |
부분 일치 검색 | contains | member.username.contains(“member”) | username like ‘%member%’ |
지정 문자열로 시작하는 부분 검색 | startsWith | member.username.startsWith(“member”) | username like ‘member%’ |
지정 문자열로 끝는 부분 검색 | endsWith | member.username.endsWith(“member”) | username like ‘%member’ |
- 이외에도 수많은 검색 조건 메소드가 존재한다.
중복 제거
distinct()
메소드를 통해 중복을 제거할 수 있다.
List<String> result =
queryFactory
.select(member.username).distinct()
.from(member)
.fetch();
결과 조회
fetch()
- 리스트 조회
- 결과가 없으면 빈 리스트를 반환한다.
fetchOne()
- 단 건 조회
- 결과가 없으면 null을 반환한다.
- 결과가 둘 이상이면 예외를 발생시킨다.
fetchFirst()
limit(1).fetchOne()
을 한 것과 같은 결과를 반환한다.
fetchResults()
- 페이징 정보를 포함한 결과를 반환한다.
- 총 개수를 조회하는 쿼리도 함께 실행된다.
- deprecated
fetchCount()
- 총 개수를 조회하는 쿼리로 변환해서 실행한다.
- deprecated
정렬
orderBy(OrderSpecifier<?>... o)
메소드를 통해 정렬한다.- orderBy 메소드 안에 정렬 방식을 나열하면 된다.
- 종류
- asc()
- 오름차순
- desc()
- 내림차순
- nullsFirst()
- 값이 null인 데이터가 전위 정렬된다.
- nullLast()
- 값이 null인 데이터가 후위 정렬된다.
- asc()
페이징
offset(long offset)
- 데이터를 읽어들이기 시작하는 위치를 지정한다.
- 기본 위치는 0부터 시작한다.
limit(long limit)
- 조회하는 데이터의 건 수를 지정한다.
fetchResults()
를 통해 페이징 정보를 가져올 수 있다.- 다만 deprecated 상태라서 추후 경우에 따라서 따로 처리를 해줘야 할 수도 있다.
그룹 함수
- 기본적인 그룹 함수
- 필드.count()
- 개수 조회
- 필드.sum()
- 합산 조회
- 필드.avg()
- 평균 조회
- 필드.max()
- 최댓값 조회
- 필드.min()
- 최솟값 조회
- 필드.count()
- 이외에도 다양한 그룹 함수를 제공한다.
- 그룹 함수를 사용할 때 Tuple을 사용하는 경우가 많다.
@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);
}
group by와 having
groupBy(Expression<?>... o)
메소드를 통해 그룹화를 할 수 있다.- groupBy 메소드 안에 그룹화할 대상을 나열하면 된다.
having(Predicate... o)
메소드를 통해 그룹화 조건을 지정할 수 있다.- having 메소드 안에 그룹화 조건을 나열하면 된다.
@Test
public void group() throws Exception {
List<Tuple> result =
queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.having(member.age.gt(20))
.fetch();
Tuple teamA = result.get(0);
assertThat(teamA.get(team.name)).isEqualTo("teamA");
assertThat(teamA.get(member.age.avg())).isEqualTo(15);
}
조인 - 기본 조인
join(EntityPath<P> target, Path<P> alias)
메소드를 통해 기본 조인을 실행한다.- target에는 조인 대상을 지정한다.
- alias에는 별칭으로 사용할 Q-Type을 지정하면 된다.
- SQL처럼 ON절을 직접 추가하지 않아도 미리 정의한 연관관계를 통해서 키 값을 매핑하는 ON절을 자동으로 작성해준다.
@Test
public void join() throws Exception {
List<Member> result =
queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
assertThat(result).extracting("username").containsExactly("member1", "member2");
}
조인 - on절
on(Predicate... conditions)
메소드를 통해 조인 조건을 추가할 수 있다.- on 메소드 안에 조인 조건을 나열하면 된다.
@Test
public void join() throws Exception {
List<Member> result =
queryFactory
.selectFrom(member)
.join(member.team, team)
.on(member.age.gt(20))
.where(team.name.eq("teamA"))
.fetch();
assertThat(result).extracting("username").containsExactly("member1", "member2");
}
조인 - 페치 조인
fetchJoin()
메소드를 통해 페치 조인을 실행할 수 있다.
@Test
public void fetchJoinUse() throws Exception {
em.flush();
em.clear();
Member findMember =
queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam()); //EntityManagerFactory를 통해 LAZY 엔티티의 초기화 여부를 알 수 있다.
assertThat(loaded).as("페치 조인 적용").isTrue();
}
서브 쿼리
- 기본적으로 Q-Type 클래스는 인스턴스를 사용하는 방법이 두 가지가 있다.
QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
- 서브 쿼리를 사용할 때는 별칭 방식을 사용해야 한다.
- JPAExpressions를 통해 서브 쿼리를 작성한다.
- 서브 쿼리를 사용하는 방식 자체는 메인 쿼리와 별차이가 없다.
@Test
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();
assertThat(result).extracting("age").containsExactly(40);
}
Case문
필드에서 직접 사용한다.
단순한 방식
member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타")
- 복잡한 방식
- 복잡한 변수를 사용할 때는 변수로 따로 선언해서 사용하는 것이 좋다.
new CaseBuilder()
.when(member.age.between(0, 20)).then("0~20살")
.when(member.age.between(21, 30)).then("21~30살")
.otherwise("기타")
상수 사용하기
- 상수를 표현해야 하는 경우에 사용한다.
Expressions.constant(T value)
메소드 안에 상수로 표현할 값을 명시하면 된다.
List<Tuple> result =
queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
문자 더하기
concat(String str)
메소드를 통해 문자열을 합칠 수 있다.
List<String> result =
queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetch();