객체 지향 쿼리 소개(JPQL, Criteria, Native, QueryDSL)
JPA를 사용하면 EntityManager.find(), 객체 그래프 탐색(member.getTeam())과 같은 기능만으로 개발하기는 어렵다.
그리고 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로 엔티티 객체를 대상으로 하는 방법이 필요하다.
JPA는 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다.
- JPQL(Java Persistence Query Language)
- Criteria 쿼리(Criteria Query) : JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
- 네이티브 SQL(Native SQL) : JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다.
- QueryDSL : Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비 표준 오픈소스 프레임워크
- JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용
JPQL(Java Persistence Query Language) 소개
JPQL은 엔티티 객체를 조회하는 객체지향 쿼이다.
SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다.
데이터베이스 방언(Dialect)만 변경하면 JPQL을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있다.
엔티티 직접 조회, 묵시적 조인, 다형성 지원으로 SQL보다 코드가 간결하다.
@Entity
public class Member {
...
@Column(name = "name")
private String username;
}
String jpql = "SELECT m FROM Member AS m WHERE m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
회원 이름이 kim인 엔티티를 조회하는데 WHERE절에 username은 테이블의 컬럼이 아니라 엔티티의 필드명이다.
코드를 실행해보면 JPA는 JPQL을 SQL을 변환헤서 데이터베이스를 조회하는것을 알수있다.
JPQL은 별칭(Member AS m)을 사용하지 않으면 오류가 발생한다.
TypeQuery, Query
작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다. 쿼리 객체는 TypeQuery와 Query가 있는데,반환할 타입을 명확하게 지정할 수 있으면 TypeQuery를, 명확하게 지정할 수 없으면 Query를 사용하면 된다.
// Member.class로 반환
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = query.getResultList();
// Object로 반환
Query query = em.createQuery("SELECT m FROM Member m");
List resultList = query.getResultList();
결과 조회
- query.getResultList()
결과를 컬렉션 형태로 반환한다. 만약 결과가 없으면 빈 컬렉션을 반환한다. - query.getSingleResult()
결과가 정확히 하나일 때 사용한다. 만약 결과가 없으면 NoResultException 예외가 발생한다.
결과가 1개보다 많으면 NoUniqueResultException 예와가 발생한다.
파라미터 바인딩
JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩도 지원한다.
이름 기준 파라미터는 이름을 기준으로 파라미터를 구분한다, 앞에 :를 붙혀 사용한다.
// 위치 기준 파라미터 바인딩
TypeQuery<Member> query =
em.createQuery("SELECT m FROM Member m WHERE m.username = ?1", Member.class);
query.setParameter(1, "kim");
List<Member> resultList = query.getResultList();
// 이름 기준 파라미터 바인딩
TypeQuery<Member> query =
em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class);
query.setParameter("username", "kim");
List<Member> resultList = query.getResultList();
DTO로 반환하기
엔티티가 아닌 DTO로 반환하려면 JPQL에 생성자를 사용하면 된다.
// 위치 기준 파라미터 바인딩
TypeQuery<MemberDTO> query =
em.createQuery("SELECT new project.package.MemberDTO(m.username) FROM Member m", Member.class);
List<MemberDTO> resultList = query.getResultList();
Criteria 쿼리 소개
Criteria는 JPQL을 생성하는 빌더 클래스다.
장점은 문자열이 아닌 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이다.
문자열로 JPQL을 작성하게 되면 오타가 있을 경우에도 배포가 진행된다.
하지만 프로그래밍 코드로 JPQL을 작성했을때는 런타임 시점에 오류가 발생해 문제가 있다는것을 미리 알 수 있다.
그리고 IDE를 사용하면 코드 자동완성도 된다.
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();
위에 코드를 보면 문자열이 아닌 코드로 작성한 것을 확인할 수 있다.
"username"을 보면 필드명을 문자로 작성했지만 메타 모델(MetaModel)을 사용하면 코드로 구현이 가능하다.
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get(Member_.username), "kim"));
Criteria가 가진 장점이 많지만 모든 장점을 상쇄할 정도로 복잡하고 장황하다.
따라서 사용하기 불편한 건 물론이고 Criteria로 작성한 코드는 한눈에 들어오지 않는다는 단점이 있다.
QueryDSL 소개
QueryDSL도 Criteria처럼 JPQL 빌더 역확을 한다.
QueryDSL의 장점은 코드 기반이면서 단순하고 사용하기 쉽다. 그래서 작성한 코드도 JPQL과 비슷해서 한눈에 들어온다
QueryDSL에 비교하면 Criteria는 너무 복잡하다.
나도 실무에선 QueryDSL만 사용하는것 같다.
QueryDSL은 JPA뿐만 아니라 JDO, 몽고DB, Java Collection, Lucene, Hibernate Search도 거의 같은 문법으로 지원한다.
나는 QueryDSL을 추천한다. 아래 블로그 글에 예제가 정리되어있다.
네이티브 SQL 소개
JPA는 SQL을 직접 사용할 수 있는 기능을 지원하는데 이것을 네이티브 SQL이라 한다.
SQL은 지원하지만 JPQL이 지원하지 않는 기능도 있고
JPQL을 사용해도 가끔은 특정 데이터베이스에 의존하는 기능을 사용해야 할 때가 있다.
예) 오라클 데이터베이스에 CONNECT BY 기능이나, 특정 데이터베이스에서만 동작하는 SQL 힌트 등등..
String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();
JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용
이런 일은 드물겠지만, JDBC 커넥션에 직접 접근하고 싶으면 JPA는 JDBC 커넥션을 획득하는 API를 제공하지 않으므로 JPA 구현체가 제공하는 방법을 사용해야 한다.
Session session = entityManager.unwrap(Session.class);
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
// work ..
}
});
먼저 JPA EntityManager에서 하이버네이트 Session을 구한다. 그리고 Session의 doWork() 메소드를 호출하면 된다.
JDBC나 MyBatis를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 하고
JPA를 우회하는 SQL에 대해서는 JPA가 전혀 인식하지 못하기 때문에 데이터 무결성을 회손할 수 있다.
이런 이슈를 해결하는 방법은 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화하면 된다.
또한 Spring AOP를 적절히 활용해서 JPA를 우회하여 데이터베이스에 접근하는 메소드를 호출할 때마다 영속성 컨텍스트를 플러시하면 위에서 언급한 문제도 깔끔하게 해결할 수 있다.