[JPA] JPQL 기본
서론
JPA에서는 여러가지 쿼리방법을 지원하는데 그 중 대표적으로 JPQL과 JPA Criteria가 있다.
JPQL의 경우에는 엔티티 객체를 대상으로 쿼리를 작성하고 SQL 문법과 매우 유사하다.
List<Member> result = em.createQuery("select m From Member m where m.name like ‘%hello%'", Member.class)
.getResultList();
하지만 String 형태로 작성되기 때문에 동적인 쿼리는 작성하기 어렵다.
이를 위해서 JPA 표준스펙으로 JPA Criteria가 존재한다. 하지만 JPA Criteria 같은 경우, 코드를 자바 코드로 작성할 수 있다는 장점이 존재한다.
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();
하지만 큰 단점으로는 너무 직관적이지가 않고 유지보수의 어려움이 존재한다. 이를 위해서 오픈소스인 QueryDSL이라는 것이 존재한다. 이를 이용하면 매우 직관적이게 동적인 쿼리를 자바 코드로 설계할 수 있다. 아래는 queryDSL의 예시 코드이다.
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;
List<Member> list =
query.selectFrom(m)
.where(m.age.gt(18))
.orderBy(m.name.desc())
.fetch();
그 외에 JPQL로는 해결이 불가능한 특정 데이터베이스의 의존 기능을 사용하려면 네이티브SQL을 사용해야하는데, 이는 em.createQuery() 가 아닌 em.createNativeQuery()를 이용해서 안에 SQL을 직접 작성한다. 또한 JDBC나 SpringTemplate, myBatis를 이용해서 함께 사용할 수 있는데, 주의해야하는 점이 있다. JPA 스펙으로 주어지는 em.createQuery나 createNativeQuery를 하면 이전에 영속성 컨택스트에 저장된 값들이 알아서 flush되는데 JPA와 관련이 없는 myBatis나 JDBC (ex) query.execute())를 쓰면 영속성 컨텍스트에 존재하는 값은 flush되지 않는다. 그렇기 때문에 예상과 다른 작동이 이루어질 수 있기 때문에 조심해야한다.
JPQL 기본
JPQL은 객체지향 쿼리 언어이다. 테이블을 대상으로 쿼리하는 것이 아닌, 엔티티 객체를 대상으로 쿼리한다. 또한 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다. 결과적으로는 SQL로 번역이 된다.
where, group by, having, order by 등등 모두 가능하다.
JPQL은 엔티티와 속성은 대소문자를 구분한다. 프로퍼티를 맞춰줘야 작동한다. 하지만 select from 과 같은 JPQL 키워드는 대소문자를 구분하지 않는다. 테이블 이름을 사용하면 안되고 엔티티 이름을 사용해서 작성해야 한다. 별칭은 무조건 지정해야한다. 아래는 예시 쿼리이다.
select m from Member as m where m.age > 18
또한 count, sum, avg, max 등과 같은 집합과 정렬의 키워드를 모두 제공한다.
em.createQuery() 안에 jpql 문법을 작성하는데, 이 .createQuery()의 반환 타입은 TypeQuery와 Query가 존재한다.
TypeQuery는 반환타입이 명확할 때 사용한다.
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class);
Query는 반환 타입이 명확하지 않을때 사용한다. 아래의 코드는 m.username과 m.age를 둘다 받음과 동시에 두번째 인자에 엔티티도 존재하지 않아서 반환 타입이 명확하지 않다.
Query query3 = em.createQuery("select m.username, m.age from Member m");
받은 TypeQuery는 getResultList를 통하여 List형태로 리턴받을 수 있다.
TypedQuery<Member> query4 = em.createQuery("select m from Member m", Member.class);
List<Member> result1 = query4.getResultList();
.getResultList()는 결과가 하나 이상일 때는 리스트를 반환하지만 결과가 존재하지 않으면 빈 리스트를 반환한다.
.getSingleResult()는 결과가 정확히 하나일때만 반환되고,
결과가 존재하지 않으면 javax.persistence.NoResultException 예외가 발생하고,
결과가 둘 이상이면 javax.persistence.NonUniqueResultException 예외가 발생한다.
근데 상식적(?)으로 결과가 둘 이상이면 그럴만 한데, 값이 없다고 예외가 터지는건 뭔가 합당하지 않다. 그래서 나중에 Spring Date JPA를 이용하면 없으면 null을 받고 할 수 있다.
아래와 같이 파라미터를 추가할 수 있다.
TypedQuery<Member> query6 = em.createQuery("select m from Member m where m.username = :username", Member.class);
query6.setParameter("username", "member1");
Member singleResult = query6.getSingleResult();
추가적으로 아래와 같이 메서드 체이닝을 할 수 있다. 이게 제일 잘 쓰이는 방식이다.
Member singleResult1 = em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
System.out.println("singleResult1 = " + singleResult1.getUsername());