빌드하면 지정한 outputDirectory에 지정한 target/generated-sources 위치에 QMember.java 처럼 Q로 시작하는 쿼리 타입들이 생성된다.
사용(4.1.3 버전 기준)
기본 사용법
동적으로 생성할 쿼리는 JPAQuery를 사용하여 만들 수 있는데, 이것보단 JPAQueryFactory를 사용하는게 권장된다고 한다.
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember member = QMember.member;
Member foundMember =
queryFactory.selectFrom(member) // select + from
.where(customer.username.eq("joont"))
.fetchOne();
대충 위의 형태로 사용할 수 있다.
결과반환
fetch : 조회 대상이 여러건일 경우. 컬렉션 반환
fetchOne : 조회 대상이 1건일 경우(1건 이상일 경우 에러). generic에 지정한 타입으로 반환
fetchFirst : 조회 대상이 1건이든 1건 이상이든 무조건 1건만 반환. 내부에 보면 return limit(1).fetchOne() 으로 되어있음
fetchCount : 개수 조회. long 타입 반환
fetchResults : 조회한 리스트 + 전체 개수를 포함한 QueryResults 반환. count 쿼리가 추가로 실행된다.
List<Member> foundMembers =
queryFactory.selectFrom(member)
.where(member.username.eq("joont")) // 1. 단일 조건
.where(member.username.eq("joont"), member.homeAddress.city.eq("seoul")) // 2. 복수 조건. and로 묶임
.where(member.username.eq("joont").or.member.homeAddress.city.eq("seoul")) // 3. 복수 조건. and나 or를 직접 명시할 수 있음
.fetch();
시작 인덱스를 지정하는 offset,
조회할 개수를 지정하는 limit,
두개를 인수로 받는 QueryModifiers를 사용하는 restrict를 지원한다.
근데 실제로 페이징 처리를 하려면 전체 데이터 개수를 알고 있어야하므로, fetchResults()를 사용해야 한다.
QueryResults<Member> result =
queryFactory.selectFrom(member)
.offset(10)
.limit(10)
.fetchResults();
List<Member> foundMembers = result.getResults(); // 조회된 member
long total = result.getTotal(); // 전체 개수
long offset = result.getOffset(); // offset
long limit = result.getLimit(); // limit
벌크 연산은 영속성 컨텍스트와 2차 캐시를 무시하고 데이터베이스에 직접 쿼리한다.
따라서 위와 같이 영속성 컨텍스트와 데이터베이스 간에 데이터 차이가 발생할 수 있는 것이다.
이를 해결하기 위한 방법은 아래와 같다.
em.refrest(entity) 사용
벌크 연산 직후에 em.refresh(entity)를 사용하여 데이터베이스에서 다시 상품을 조회하면 된다.
벌크 연산 먼저 실행
벌크 연산을 가장 먼저 실행하면 이미 변경된 내용을 데이터베이스에서 가져온다.
가장 실용적인 해결책이다.
벌크 연산 수행 후 영속성 컨텍스트 초기화
영속성 컨텍스트가 초기화되면 데이터베이스에서 다시 조회해오기 때문에 이것도 방법이다.
영속성 컨텍스트와 JPQL
영속성 컨텍스트에 이미 있는 엔티티를 JPQL로 다시 조회해올 경우 어떻게 처리될까?
Member member1 = em.find(Member.class, 1);
List<Member> list =
em.createQuery("SELECT m FROM Member m", Member.class)
.getResultLst();
이미 영속성 컨텍스트에 들어있는 1번 member가 JPQL에 의해 다시 한번 조회되는 상황이다.
결과부터 말하자면 JPQL 쿼리는 쿼리대로 다 날라가고, 조회한 엔티티를 영속성 컨텍스트에 다 저장한다.
여기서 중요한 점은 1번 member의 경우 영속성 컨텍스트에 이미 들어있으므로, JPQL로 조회해온 1번 member는 그냥 버려진다는 것이다.
보다시피 조회해온 member들 중 1번 member는 영속성 컨텍스트에 이미 있으므로 그 결과가 버려진다.
영속성 컨텍스트에 없는 2번 member의 경우 영속성 컨텍스트에 저장된다.
새로 조회해온 결과를 기존 영속성 컨텍스트에 덮어쓰지 않는 이유는 영속 상태인 엔티티의 동일성을 보장해야하기 때문이다.
find로 들고오든, JPQL로 들고오든 동일한 엔티티를 반환해야 한다.
Member member1 = em.find(Member.class, 1);
Member member2 =
em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
.setParameter("id", 1)
.getSingleResult();
assertSame(member1, member2); // SUCCESS
보다시피 영속성 컨텍스트에 1번 member 엔티티가 있더라도 무조건 SQL을 실행해서 조회해온다.
(JPQL을 분석해서 영속성 컨텍스트를 조회하는 것은 너무 힘들기 때문이다.)
그리고 조회해온 엔티티를 영속성 컨텍스트에 넣을 때, 이미 있는 엔티티일 경우 결과를 버린다.
JPQL과 플러시 모드
플러시 모드는 FlushMode.AUTO(Default), FlushMode.COMMIT이 있다.
이때까지 FlushMode.AUTO 는 트랜잭션이 끝날때나 커밋될 때만 플러시를 호출하는 것으로 알고 있었으나, 사실은 시점이 하나 더 있다. JPQL 쿼리를 실행하기 직전이다.
Member member1 = em.find(Member.class, 1);
member1.setName("modified name");
Member member2 =
em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
.setParameter("id", 1)
.getSingleResult();
assertThat(member1.getName(), member2.getName());
변경감지는 플러시 될때 발생하므로, JPQL에서 아직 변경되지 않은 name 값을 가진 1번 member를 가져올 것이라고 생각할 수 있지만, FlushMode.AUTO는 영속 상태인 엔티티의 동일성을 보장하기 위해 JPQL 실행 전에 플러시를 수행한다.
그러므로 위의 테스트는 성공한다.
어떻게 동작하는지 정확히는 모르겠으나, 영속성 컨텍스트에 있는 엔티티에 대해 JPQL을 실행할 떄만 플러시를 수행한다.
즉, 위의 상황에서 JPQL로 Team을 조회해올 경우 플러시가 발생하지 않는다.
하지만 이 상황에서 FlushMode.COMMIT으로 설정하면 쿼리전에 플러시를 수행하지 않으므로 위의 테스트가 실패하게 된다.
이때는 직접 em.flush를 호출해주거나, Query 객체에 직접 플러시 모드를 설정해주면 된다.
em.setFlushMode(FlushMode.COMMIT); // 커밋시에만 플러시
Member member1 = em.find(Member.class, 1);
member1.setName("modified name");
em.flush(); // 1. em.flush 직접 호출
Member member2 =
em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
.setParameter("id", 1)
.setFlushMode(FlushMode.AUTO) // 2. setFlushMode 설정
.getSingleResult();
assertThat(member1.getName(), member2.getName());
FlushMode.COMMIT은 너무 잦은 플러시가 일어나는 경우, 플러시 횟수를 줄여서 성능을 최적화하고자 할 때 사용할 수 있다.
JPQL을 편하게, 동적으로 작성할 수 있도록 JPA에서 공식 지원하는 Creteria 라는것이 있다.
하지만 큰 단점이 있는데, 너무 불편하다는 것이다.
그에 반해 JPA에서 공식 지원하지는 않지만
쿼리를 문자가 아닌 코드로 작성해도 쉽고 간결하며, 모양도 쿼리와 비슷하게 개발할 수 있는 QueryDSL 이라는 것이 있다.
QueryDSL은 오픈소스 프로젝트이며, 이름 그대로 데이터를 조회하는데 기능이 특화되어 있다.
설정
필요 라이브러리
쿼리 타입
엔티티를 기반으로 생성된 쿼리용 클래스를 말한다.
빌드하면 지정한
outputDirectory에 지정한 target/generated-sources
위치에QMember.java
처럼 Q로 시작하는 쿼리 타입들이 생성된다.사용(4.1.3 버전 기준)
기본 사용법
동적으로 생성할 쿼리는
JPAQuery
를 사용하여 만들 수 있는데, 이것보단JPAQueryFactory
를 사용하는게 권장된다고 한다.대충 위의 형태로 사용할 수 있다.
결과반환
return limit(1).fetchOne()
으로 되어있음프로젝션
프로젝션을 지정한다.
(아직 나오진 않았지만 from 절에 위처럼 쿼리 타입을 연속으로 줄 경우, 두 엔티티가 조인된다.)
member와 order가 조인된 상태에서 member 엔티티의 속성만 가져온다.
(select를 생략하면 기본적으로 from의 첫번째 엔티티를 프로젝션 대상으로 쓴다)
from
쿼리할 대상을 지정한다.
member 테이블을 전체 조회하게 된다. 프로젝션 지정(select)가 빠졌지만 위와 동일하게 from의 첫번째 엔티티를 사용한다.
조인
join, innerJoin, leftJoin, rightJoin 을 지원한다.
개인적으로 from절에 multiple arguments를 주는것보다 이게 더 좋다.(SQL에서도...)
join의 첫번쨰 인자로는 join할 대상, 두번쨰 인자로는 join할 대상의 쿼리 타입을 주면 된다. on 절은 자동으로 붙는다.
조건
그룹핑
group by도 가능하다.
city로 group by 한 뒤 city만 출력하게 된다.
정렬
페이징
시작 인덱스를 지정하는
offset
,조회할 개수를 지정하는
limit
,두개를 인수로 받는 QueryModifiers를 사용하는
restrict
를 지원한다.근데 실제로 페이징 처리를 하려면 전체 데이터 개수를 알고 있어야하므로, fetchResults()를 사용해야 한다.
다중 결과 반환
다중 프로젝션 할 경우 Tuple 클래스로 받을 수 있다.
class com.querydsl.core.types.QTuple$TupleImpl
클래스가 리턴되는데 이것보단 아래 bean population을 쓰는게 더 나아보인다.서브쿼리
동적 조건
수정, 삭제, 배치 쿼리
벌크 연산(UPDATE, DELETE)
JPQL로 여러 건을 한번에 수정하거나 삭제할 떄 사용한다.
아래는 UPDATE 벌크 연산이다.
executeUpdate
메서드를 사용한다. 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다.아래는 DELETE 벌크 연산이다.
벌크 연산시 주의사항
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 특징이 있으므로 주의해야 한다.
아래는 발생가능한 문제 상황이다.
벌크 연산은 영속성 컨텍스트와 2차 캐시를 무시하고 데이터베이스에 직접 쿼리한다.
따라서 위와 같이 영속성 컨텍스트와 데이터베이스 간에 데이터 차이가 발생할 수 있는 것이다.
이를 해결하기 위한 방법은 아래와 같다.
em.refrest(entity)
사용영속성 컨텍스트와 JPQL
영속성 컨텍스트에 이미 있는 엔티티를 JPQL로 다시 조회해올 경우 어떻게 처리될까?
이미 영속성 컨텍스트에 들어있는 1번 member가 JPQL에 의해 다시 한번 조회되는 상황이다.
결과부터 말하자면 JPQL 쿼리는 쿼리대로 다 날라가고, 조회한 엔티티를 영속성 컨텍스트에 다 저장한다.
여기서 중요한 점은 1번 member의 경우 영속성 컨텍스트에 이미 들어있으므로, JPQL로 조회해온 1번 member는 그냥 버려진다는 것이다.
보다시피 조회해온 member들 중 1번 member는 영속성 컨텍스트에 이미 있으므로 그 결과가 버려진다.
영속성 컨텍스트에 없는 2번 member의 경우 영속성 컨텍스트에 저장된다.
새로 조회해온 결과를 기존 영속성 컨텍스트에 덮어쓰지 않는 이유는
영속 상태인 엔티티의 동일성을 보장해야하기 때문
이다.보다시피 영속성 컨텍스트에 1번 member 엔티티가 있더라도 무조건 SQL을 실행해서 조회해온다.
(JPQL을 분석해서 영속성 컨텍스트를 조회하는 것은 너무 힘들기 때문이다.)
그리고 조회해온 엔티티를 영속성 컨텍스트에 넣을 때, 이미 있는 엔티티일 경우 결과를 버린다.
JPQL과 플러시 모드
플러시 모드는
FlushMode.AUTO(Default)
,FlushMode.COMMIT
이 있다.이때까지
FlushMode.AUTO
는 트랜잭션이 끝날때나 커밋될 때만 플러시를 호출하는 것으로 알고 있었으나, 사실은 시점이 하나 더 있다. JPQL 쿼리를 실행하기 직전이다.변경감지는 플러시 될때 발생하므로, JPQL에서 아직 변경되지 않은 name 값을 가진 1번 member를 가져올 것이라고 생각할 수 있지만,
FlushMode.AUTO
는 영속 상태인 엔티티의 동일성을 보장하기 위해JPQL 실행 전에 플러시를 수행한다
.그러므로 위의 테스트는 성공한다.
하지만 이 상황에서
FlushMode.COMMIT
으로 설정하면 쿼리전에 플러시를 수행하지 않으므로 위의 테스트가 실패하게 된다.이때는 직접
em.flush
를 호출해주거나, Query 객체에 직접 플러시 모드를 설정해주면 된다.FlushMode.COMMIT은 너무 잦은 플러시가 일어나는 경우, 플러시 횟수를 줄여서 성능을 최적화하고자 할 때 사용할 수 있다.