본 글은 김영한님의 <자바 ORM 표준 JPA 프로그래밍>을 읽고 공부한 내용을 정리한 글입니다.
- 객체지향 쿼리 소개
- JPQL
- Criteria
- QueryDSL
- 네이티브 SQL
- 객체지향 쿼리 심화
Criteria 쿼리는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API이다.
장점
단점
Criteria 쿼리를 생성하기 위해서 먼저 Criteria 빌더를 얻어야 한다.
CriteriaBuilder cb = em.getCriteriaBuilder();
Criteria 쿼리 빌더에서 Criteria 쿼리를 생성하고, 반환 타입을 지정할 수 있다.
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
FROM 절에서 반환된 값 m은 조회의 시작점이라는 의미로 쿼리 루트라고 한다.
Root<Member> m = cq.from(Member.class);
SELECT 절을 생성한 뒤는 JPQL과 같다.
cq.select(m);
Root<Member> m = cq.from(Member.class);
여기서 m이 쿼리 루트이다.m.get("username")
은 JPQL의 m.username
과 같다.m.get("team").get("name")
은 JPQL의 m.team.name
과 같다.동적 쿼리란 다양한 검색 조건에 따라 실행 시점에 쿼리를 생성하는 것이다.
Criteria로 동적 쿼리를 구성하면 JPQL과 달리 공백이나 where, and의 위치로 인해 에러가 발생하지는 않는다.
하지만 Criteria의 장황하고 복잡함으로 인해, 코드가 읽기 힘들다는 단점이 있다.
Criteria는 코드 기반이므로 컴파일 시점에 오류를 발견할 수 있지만 파라미터는 문자이기 때문에 오타가 나면 에러를 발견하지 못한다.
이런 부분까지 코드로 작성하기 위해서 메타 모델 API를 사용한다.
메타 모델을 적용하기 전
cq.select(m).where(cb.gt(m.<Integer>get("username"), 20)).orderBy(cb.desc(m.get("age")));
메타 모델을 적용한 후
cq.select(m).where(cb.gt(m.get(Member_.age), 20)).orderBy(cb.desc(m.get(Member_.age)));
이처럼 문자 기반에서 정적인 코드 기반으로 변경된 것을 확인할 수 있다.
QueryDSL는 쿼리를 문자가 아닌 코드로 작성하고, 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는 프로젝트이다.
먼저 JPQQuery 객체를 생성한다.
JPAQuery query = new JPAQuery(em);
사용할 쿼리 타입(Q)을 생성하고 별칭을 준다.
QMember qMember = new QMember("m");
다음은 쉽다.
List<Member> members = query.from(qMember).where(qMember.name.eq("회원1")).orderBy(qMember.name.desc()).list(qMember);
쿼리 타입은 사용하기 편리하도록 기본 인스턴스를 보관하고 있다.
하지만 같은 엔티티를 조인하거나 같은 엔티티를 서브쿼리에 사용하면 같은 별칭이 사용되므로 이 때는 별칭을 직접 지정해서 사용해야 한다.
QMember qMember = new QMember("m"); //직접 지정
QMember qMember = QMember.member; // 기본 인스턴스 사용
QueryDsl의 where 절에는 and나 or을 사용할 수 있다.
아래처럼 ,
를 이용해서 여러 검색 조건을 사용한다면 and 연산이 된다.
.where(item.name.eq("좋은 상품"), item.price.gt(20000))
쿼리 타입의 필드는 필요한 대부분의 메소드를 명시적으로 제공한다.
item.price.between(1000, 2000)
item.name.contains("상품1")
: SQL에서 like '%상품1%' 검색
item.name.startsWith("고급")
: SQL에서 like '고급%' 검색
대표적인 결과 조회 메소드는 다음과 같다.
uniqueResult()
: 조회 결과가 한 건일 때 사용한다.singleResult()
: 결과가 하나 이상이면 처음 데이터를 반환한다.list()
: 결과가 하나 이상일 때 사용한다. 결과가 없으면 빈 컬렉션을 반환한다.JPQL은 특정 데이터베이스에 종속적인 기능을 지원한다.
특정 데이터베이스만 사용하는 함수
특정 데이터베이스만 지원하는 SQL 쿼리 힌트
인라인 뷰, UNION, INTERSECT
스토어 프로시져
특정 데이터베이스만 지원하는 문법
위처럼 SQL을 직접 사용할 수 있는 기능을 네이티브 SQL이라고 한다.
네이티브 SQL을 사용하면 엔티티를 조회할 수 있고, JPA가 지원하는 영속성 컨텍스트 기능을 그대로 사용할 수 있다.
네이티브 쿼리 API 3가지
결과 타입 정의
public Query createNativeQuery(String sqlString, Class resultClass);
결과 타입을 정의할 수 없을 때
public Query createNativeQuery(String sqlString);
결과 매핑 사용
public Query createNativeQuery(String sqlString, String resultSetMapping);
네이티브 SQL로 SQL만 직접 사용할 뿐이지 나머지는 JPQL과 같다.
조회한 엔티티도 영속성 컨텍스트에서 관리한다.
될 수 있으면 표준 JPQL을 사용하고 기능이 부족하면 차선책으로 하이버네이트와 같은 JPA 구현체 기능을 사용하는 것이 좋다.
그래도 안 되면 마지막 방법으로 네이티브 SQL을 사용하고, SQL 매퍼와 JPA를 함께 사용하는 것도 고려할 만하다.
벌크 연산은 한 번에 여러 데이터를 수정하거나 삭제할 수 있는 기능이다.
executeUpdate()
메소드를 사용한다.
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다.
따라서 영속성 컨텍스트에 있는 상품1과 데이터베이스에 있는 상품1의 가격이 달라지는 문제가 발생할 수도 있다.
이 문제를 해결하기 위한 방법이 세 가지 있다.
em.refresh()
사용
벌크 연산 먼저 실행
벌크 연산 수행 후 영속성 컨텍스트 초기화
JPQL로 엔티티, 임베디드 타입, 값 타입을 조회할 수 있지만 영속성 컨텍스트에 관리되는 것은 엔티티 뿐이다.
영속성 컨텍스트에 이미 회원1이 있는데, JPQL로 회원1을 다시 조회한다면
JPQL로 데이터베이스에서 조회한 결과를 버리고 대신에 영속성 컨텍스트에 있던 엔티티를 반환한다.
기존 엔티티를 새로 검색한 엔티티로 대체하는 방법도 생각해볼 수 있겠으나, 영속성 컨텍스트에 수정 중인 데이터가 사라질 수도 있으므로 위험하다.
em.find()
메소드는 영속성 컨텍스트에서 엔티티를 먼저 찾고, 없으면 데이터베이스를 조회한다.
하지만 JPQL은 항상 데이터베이스를 먼저 조회한다.
플러시는 영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화하는 것이다.
플러시 모드의 기본값인 FlushModeType.AUTO
에 ㄸ라 커밋하기 직전이나 쿼리 실행 직전에 자동으로 플러시가 호출된다.
JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 바로 데이터베이스에서 데이터를 조회한다.
따라서 JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해야 한다.