10. 객체지향 쿼리 언어
객체지향 쿼리 소개
- JPA는 복잡한 검색 조건을 사용해 엔티티 객체를 조회하는 다양한 쿼리 기술을 지원한다.
JPQL
- 테이블이 아닌 엔티티 객체를 대상으로 검색하는 객체지향 쿼리다.
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
String jpql = "SELECT m FROM Member as m WHERE m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
Criteria
- JPQL을 편하게 작성하도록 도와주는 빌더 클래스로 문자가 아닌 프로그래밍 코드로 JPQL을 작성한다.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
Root<Member> m = query.from(Member.class);
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));
List<Member> resultList = em.createQuery(cq).getResultList();
장점
- 컴파일 시점에 오류 발견
- IDE를 통해 코드 자동완성
- 동적 쿼리 작성 용이
단점
- 복잡하고 사용하기 불편함
- 직관적이지 않음
QueryDSL
JPQL을 편하게 작성하도록 도와주는 빌더 클래스로 문자가 아닌 프로그래밍 코드로 JPQL을 작성한다.
pom.xml 설정
// 쿼리타입 생성 플러그인 <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/querydsl</outputDirectory> <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin>
// 쿼리 타입을 생성할 때 필요한 라이브러리 <dependency> <groupId>com.mysema.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>3.6.3</version> <scope>provided</scope> </dependency>
// QueryDSL JPA 라이브러리 <dependency> <groupId>com.mysema.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>3.6.3</version> </dependency>
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member; // 기본 인스턴스 사용
List<Member> members = query.from(member)
.where(member.username.eq("kim"))
.list(member);
장점
- 컴파일 시점에 오류 발견
- IDE를 통해 코드 자동완성
- 동적 쿼리 작성 용이
- 단순하고 사용하기 편함
- 직관적임
Native SQL
- SQL을 직접 사용할 수 있다.
- 특정 데이터베이스에 의존하는 기능을 사용할 때 사용한다.
- 데이터베이스를 변경하면 Native SQL도 수정해야 한다.
String sql = "SELECT id, age, team_id, name FROM Member WHERE name = 'kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();
QueryDSL
결과 조회
- uniqueResult() : null or 하나의 데이터 반환 or NonUniqueResultException
- singleResult() : null or 첫 데이터 반환
- list() : 빈 컬렉션 or 목록 반환
조건 검색
query.from(member)
.where(member.name.eq("Bob").or(member.name("Jo"))
.list(member);
// JPQL
select member
from Member member
where member.name = ? or member.name = ?
페이징과 정렬
query.from(item)
.orderBy(item.price.desc(), item.name.asc())
.offset(10).limit(10)
.list(item);
// JPQL
select item
from Item item
order by item.price desc, item.name asc
limit ?, ?
그룹
query.from(item)
.groupBy(item.price)
.having(item.price.gt(1000))
.list(item);
// JPQL
select item
from Item item
group by item.price
having item.price > ?
조인
query.from(order)
.leftJoin(order.orderItems, orderItem)
.on(orderItem.price.gt(2000))
.list(order);
// JPQL
select order
from Order order left join order.orderItems orderItem
on orderItem.price > ?
페치조인
- JPQL에서 성능 최적화를 위해 제공
- 연관된 엔티티나 컬렉션을 한 번에 같이 조회
- 글로벌 로딩전략 FetchType.LAZY 보다 우선순위라 필요부분에 즉시로딩 가능
엔티티 페치조인 - 연관된 엔티티를 함께 조회
query.from(member)
.innerJoin(member.team, team).fetch()
.list(member);
// JPQL
select member
from Member member inner join fetch member.team
컬렉션 페치조인 - 일대다 관계의 컬렉션을 함께 조회
query.from(team)
.innerJoin(team.members, member).fetch()
.where(team.name.eq('A'))
.list(team);
// JPQL
select team
from Team team join fetch team.members
where team.name = ?
서브 쿼리
QItem item = QItem.item;
QItem i = new QItem("i");
query.from(item)
.where(item.price.eq(
new JPASubQuery().from(i).unique(i.price.max())
))
.list(item);
// JPQL
select item
from Item item
where item.price = ( select max(i.price)
from Item i )
동적 쿼리
...
if (search.isDateSearch()) {
query.where(partner.createdDt.between(startDate, DateUtil.addDays(endDate, 1)));
}
if (StringUtils.isNotBlank(search.getKeyword())) {
query.where(partner.name.like(search.getKeyword() + "%"));
}
if (StringUtils.isNotBlank(search.getCountry())) {
query.where(partner.countryCode.eq(search.getCountry()));
}
if (StringUtils.isNotBlank(search.getStatus())) {
query.where(partner.status.eq(search.getStatus()));
}
...
수정/삭제 배치 쿼리
- 한 번에 여러 데이터 수정/삭제
QItem item = QItem.item;
JPAUpdateClause updateClause = new JPAUpdateClause(em, item);
updateClause.where(item.name.eq("A"))
.set(item.price, 2000)
.execute();
JPADeleteClause deleteClause = new JPADeleteClause(em, item);
deleteClause.where(item.name.eq.("A"))
.execute();
// JPQL
update Item item
set item.price = ?
where item.name = ?
delete from Item item
where item.name = ?
객체지향 쿼리 심화
벌크 연산
- 영속성 컨텍스트를 무시하고 DB에 직접 쿼리
- 벌크 연산 직후 em.refresh()
- 벌크 연산 먼저 실행
- 벌크 연산 직후 영속성 컨텍스트 초기화
영속성 컨텍스트와 JPQL
- JPQL은 항상 DB 직접 조회
- JPQL로 조회한 엔티티는 영속 상태
- 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 DB에서 조회한 결과를 버리고 기존 엔티티 반환
- 영속성 컨텍스트는 영속 상태인 엔티티의 동일성 보장
JPQL과 플러시 모드
- 플러시 : 영속성 컨텍스트의 변경 내용을 DB에 반영
- 쿼리에 설정하는 플러시 모드는 엔티티 매니저에 설정하는 플러시 모드보다 우선권 소유
em.setFlushMode(FlushModeType.AUTO) 커밋orJPQL시 플러시
em.createQuery(jpql, Member.class).setFlushMode(FlushModeType.COMMIT) 커밋시 플러시