객체지향 쿼리 언어
- JPA는 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술인 JPQL, Criteria, QueryDSL 등을 지원
- Criteria나 QueryDSL은 결국 JPQL을 편리하게 사용하도록 도와주는 기술
객체지향 쿼리 소개
- 가장 단순한 검색 방법으로는 식별자로 조회와 객체 그래프 탐색이 존재하며
식별자로 엔티티 하나를 조회한 후, 조회한 엔티티에 객체 그래프 탐색을 사용하면 연관된 엔티티들을 찾을 수 있음
- 식별자로 조회 : EntityManager.find()
- 객체 그래프 탐색 : a.getB().getC()
- 하지만 위의 방법만으로는 애플리케이션을 개발하기 힘드므로 좀 더 현실적이고 복잡한 검색 방법인 JPQL이 필요
- SQL이 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리라면 JPQL은 엔티티 객체를 대상으로 하는 객체지향 쿼리
JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한 후, 조회한 결과로 엔티티 객체를 생성해서 반환
- JPQL의 특징
- 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음
- JPA가 공식 지원하는 다양한 검색 방법
- JPQL (Java Persistence Query Language)
- Critera 쿼리 : JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
- 네이티브 SQL : JPA에서 JPQL 대신 직접 SQL을 작성할 수 있음
- 이외에 알아둘 기능
- QueryDSL : Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크
- JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용 : 필요하면 JDBC를 직접 사용할 수 있음
JPQL 소개
- 엔티티 객체를 조회하는 객제치향 쿼리
- JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않으므로
데이터베이스 방언만 변경하면 JPQL을 수정하지 않아도 데이터베이스를 쉽게 변경할 수 있음
즉, JPQL이 제공하는 표준화된 함수를 사용하면 선택한 방언에 따라 해당 데이터베이스에 맞춘 적절한 SQL 함수가 실행됨
- JPQL은 엔티티 직접 조회, 묵시적 조인, 다형성 지원으로 SQL보다 코드가 간결
// 회원 엔티티
@Entity(name="Member") // name 속성의 기본값은 클래스 명
public class Member {
@Column(name = "name")
private String username;
// ...
}
// JPQL 사용
// JPA는 JPQL을 SQL로 변환해서 데이터베이스를 조회한 후, 조회한 결과로 Member 엔티티를 생성해서 반환
// 회원이름이 kim인 엔티티를 조회하는 쿼리 생성
String jpql = "select m from Member as m where m.username = 'kim'"; // 엔티티 객체의 필드명으로 조회
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
// 실제 실행된 SQL
select
member.id as id,
member.age as age,
member.team_id as team,
member.name as name
from
Member member
where
member.name='kim'
Criteria 쿼리 소개
- JPQL을 생성하는 빌더 클래스
- 문자가 아닌 query.select(m).where(...)처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 장점 존재
- JPQL에서 쿼리에 오타가 있을 경우 컴파일은 성공해 애플리케이션을 서버에 배포할 수 있지만,
해당 쿼리가 실행되는 런타임 시점에 오류가 발생하는 문자기반 쿼리의 단점이 존재
- 반면에 Criteria는 문자가 아닌 코드로 JPQL을 작성하므로 컴파일 시점에 오류를 발견할 수 있으며
IDE를 사용하면 코드 자동완성을 지원하며 동적 쿼리를 작성하기 편하다는 장점이 존재
- 위 JPQL을 Criteria로 작성하며, 메타 모델 API를 이용해서 필드명도 코드로 작성하여 온전히 코드만 사용해서 쿼리를 작성
메타 모델 API는 자바가 제공하는 어노테이션 프로세서 기능을 사용하면 어노테이션을 분석해서 클래스를 생성할 수 있으며
JPA는 이를 사용해서 Member 엔티티 클래스로부터 Member_라는 Criteria 전용 클래스를 생성하며 이를 메타 모델이라고 함
// Criteria 쿼리
// Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
// 루트 클래스 (조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);
// 쿼리 생성 (메타 모델 사용 전)
// 쿼리를 문자가 아닌 코드로 작성
/* 아쉬운 점은 m.get("username")을 보면 필드 명을 문자로 사용했으므로
이 부분도 문제가 아닌 코드로 작성하고 싶다면 메타 모델을 사용 */
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));
List<Member> resultList = em.createQuery(cq).getResultList();
// 쿼리 생성 (메타 모델 사용 후)
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get(Member_.username), "kim"));
List<Member> resultList = em.createQuery(cq).getResultList();
- Criteria가 가진 장점이 많지만 모든 장점을 상쇄할 정도로 복잡하고 장황하며 한 눈에 들어오지 않는다는 단점이 존재
QueryDSL 소개
- Criteria처럼 JPQL 빌더 역할을 하는 오픈소스 프로젝트
- 코드가 기반이면서 단순하고 사용하기 쉬운 장점이 존재
- Criteria와 달리 작성한 코드가 JPQL과 비슷하므로 한 눈에 들어옴
- QueryDSL도 어노테이션 프로세서를 사용해서 쿼리 전용 클래스를 생성
// QueryDSL 코드
// 준비
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member; // Member 엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스
// 쿼리, 결과조회
List<Member> members = query.from(member).where(member.username.eq("kim")).list(member);
네이티브 SQL 소개
- SQL을 직접 사용할 수 있는 기능
- JPQL을 사용해도 가끔은 특정 데이터베이스에 의존하는 기능을 사용해야 할 때가 있음
예) 오라클 데이터베이스의 CONNECT BY 기능, 특정 데이터베이스에서만 동작하는 SQL 힌트
하지만 이런 기능들은 전혀 표준화되어 있지 않으므로 JPQL에서 사용할 수 없으므로 이 때 네이티브 SQL을 사용
- 네이티브 SQL은 특정 데이터베이스에 의존하는 SQL을 작성해야 하므로 데이터베이스를 변경하면 네이티브 SQL도 수정해야 함
// 네이티브 SQL
// 직접 작성한 SQL을 데이터베이스에 전달
String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();
JDBC 직접 사용, 마이바티스 같은 SQL 매퍼 프레임워크 사용
- JDBC 커넥션에 직접 접근하고 싶으면 JPA는 JDBC 커넥션을 획득하는 API를 제공하지 않으므로
JPA 구현체가 제공하는 방법을 사용해야 함
- 하이버네이트에서 직접 JDBC Connection을 획득하는 방법
// 하이버네이트 JDBC 획득
// JPA EntityManger에서 하이버네이트 Session을 구한 후 Session의 doWork() 메소드를 호출
Session session = entityManager.unwrap(Session.class);
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
// work...
}
});
- JDBC나 마이바티스를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 함
JDBC를 직접 사용하든 마이바티스 같은 SQL 매퍼와 사용하든 모든 JPA를 우회해서 데이터베이스 접근함
그러므로 JPA를 우회하는 SQL에 대해서는 JPA가 전혀 인식하지 못하므로
영속성 컨텍스트와 데이터베이스를 불일치 상태로 만들어 데이터 무결성을 훼손할 수 있음
예) 같은 트랜잭션에서 영속성 컨텍스트에 있는 10000원짜리 상품 A의 가격을 9000원으로 변경하고 아직 플러시하지 않았을 때
JPA를 우회해서 데이터베이스에 직접 상품 A를 조회하면 상품 가격이 데이터베이스에는 아직 10000원이므로 10000원이 조회
이러한 이슈를 해결하기 위해 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서
데이터베이스와 영속성 컨텍스트를 동기화시켜야 함
스프링 프레임워크의 경우 AOP를 적절히 활용해서 JPA를 우회해서 데이터베이스에 접근하는 메소드를 호출할 때마다
영속성 컨텍스트를 플러시하면 문제를 해결할 수 있음