JPA는 다양한 쿼리 방법을 지원한다.
그 중 대표적으로 사용되는 것이 JPQL과 QueryDSL이다.
이번 게시글 부터 다음 게시글까지 JPQL의 개념과 사용 방법에 대해 알아보고자 한다.
JPQL
JPQL의 특징
- JPQL은 JPA가 제공하는 기능이며, SQL을 추상화한 객제 지향 쿼리이다.
- JPQL은 SQL을 추상화 했기 때문에, 특정 데이터베이스의 SQL 문법에 의존하지 않는다.
- SQL은 데이터베이스 테이블을 대상으로 쿼리를 작성하지만, JPQL은 엔티티 객체를 대상으로 쿼리를 작성한다.
- JPQL은 설정된 Dialect(벤더별 방언)을 참고하여, 결국 SQL로 변환된다.
JPQL 사용 방법
앞으로의 설명과 예시 코드는 아래의 객체 및 데이터 모델을 기반으로 설명할 것이다.
JPQL 기본 규칙
select m from Member as m where m.age > 18이라는 JPQL 쿼리 예시를 통해 기본 규칙을 알아보자.
- 테이블의 이름이 아닌, 엔티티의 이름(Member)을 사용해야한다.
- 엔티티(Member)와 속성(m.age)은 대소문자를 구분하여 작성해야한다.
- 엔티티(Member)에 대한 별칭(m)은 필수이다. as는 생략 가능하다.
- 키워드(SELECT, select)는 대소문자를 구분하지 않는다.
- select, from, where, group by, having, join을 지원한다.
데이터 조회 결과 반환 타입
JPQL의 데이터 조회 결과 반환 타입은 TypeQuery, Query가 있다.
- TypeQuery: 반환 타입이 명확할 때 사용한다.
- Query: 반환 타입이 명확하지 않을 때 사용한다.
TypeQuery 또는 Query로부터 실제 결과를 반환하는 메소드는 getResultList(), getSingleResult()가 있다.
- getResultList(): 결과가 1개 이상일 때 사용한다. 리스트를 반환한다(데이터가 없으면 빈 리스트 반환).
- getSingleResult(): 결과가 1개일 때 사용한다. 결과가 없거나, 2개 이상이면 예외가 발생한다. 단일 객체를 반환한다.
아래의 예시 코드를 살펴보자.
EntityManager.createQuery(쿼리, 리턴 타입)을 통해 해당 쿼리를 실행하고, TypeQuery 또는 Query를 반환 받는다.
TypeQuery와 Query의 차이점은 리턴 타입의 명확성이라고 앞서 말했을 것이다.
리턴 타입의 명확성이라는 것은 createQuery(쿼리, 리턴 타입)을 사용할 때, 리턴 타입 파라미터의 유무에 따라 결정된다.
TypeQuery<Member> typeQuery = em.createQuery("select m from Member m", Member.class)를 살펴보자.
Member 객체로 데이터를 반환 받기 위해 createQuery(쿼리, Member.class)라고 리턴 타입 파라미터를 사용했다.
그리고 그 결과 TypeQuery<Member>로 데이터 조회 결과가 반환된다.
반대로 Query query = em.createQuery("select m.username, m.age from Member m")을 살펴보자.
createQuery(쿼리)에서 리턴 타입 파라미터를 사용하지 않았다.
그 결과 Query 타입으로 데이터 조회 결과가 반환된다.
그리고 getResultList() 또는 getSingleResult()를 통해 실제 결과를 반환하는 것을 볼 수 있다.
파라미터 바인딩과 체이닝을 활용하는 예시 코드도 포함되어 있으니 참고하자.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);
// TypeQuery: 리턴 타입이 명확할 때
TypedQuery<Member> typeQuery = em.createQuery("select m from Member m",
Member.class);
// Query: 리턴 타입이 명확하지 않을 때
Query query = em.createQuery("select m.username, m.age from Member m");
// 결과가 여러개 일 때 - 결과가 없으면 빈 리스트 반환
List<Member> typeQueryResultList = typeQuery.getResultList();
typeQueryResultList.stream()
.forEach(s -> System.out.println(s.getUsername()));
// 결과가 정확히 하나일 때 - 결과가 없거나, 둘 이상이면 예외 발생
Member typeQuerySingleResult = typeQuery.getSingleResult();
// 파라미터 바인딩 + 체이닝 활용
Member result = em.createQuery(
"select m from Member m where m.username = :username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
System.out.println(
"parameterQuerySingleResult = " + result.getUsername());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
프로젝션
프로젝션이란 select 절에 지정된 조회 대상을 말한다.
프로젝션 종류로는 엔티티, 임베디드 타입, 스칼라 타입(기본 데이터 타입)이 있다.
- select m from Member m: m은 Member 엔티티 객체이므로 엔티티 프로젝션이다.
- select m.team from Member m: Member의 참조 객체 team은 Team 엔티티 객체이다. 그러므로 m.team은 엔티티 프로젝션이다.
- select m.address from Member m: Member의 참조 객체 address는 Address 임베디드 타입 객체이다. 그러므로 m.address는 임베디드 타입 프로젝션이다.
- select m.username, m.age from Member m: username, age는 Member의 기본 타입 속성이다. 그러므로 m.username, m.age는 스칼라 타입 프로젝션이다.
아래의 예시 코드를 통해 프로젝션에 대해 이해해보자.
엔티티 프로젝션 try ~ catch 내부를 살펴보자.
엔티티 프로젝션에서 중요한 점은 엔티티 프로젝션으로 가져온 엔티티 객체들은 모두 영속성 컨텍스트에서 관리된다는 것 이다.
임베디드 프로젝션 try ~ catch 내부를 살펴보자.
o.address 프로젝션은 Address 타입이기 때문에 리턴 타입을 Address로 명시할 수 있다.
마지막으로 스칼라 프로젝션을 살펴보자.
스칼라 프로젝션의 데이터 조회 결과를 가져오는 방법은 크게 3가지가 있다.
1. Object 타입 반환
스칼라 프로젝션의 데이터 조회 결과는 기본적으로 Object 타입으로 반환된다.
이 Obejct을 Object[]로 캐스팅하면, 해당 배열에는 스칼라 프로젝션 순서대로 데이터가 저장되어있다.
2. 제네릭을 통한 캐스팅 생략
제네릭을 통해 데이터 조회 결과를 애초에 Object[] 타입으로 반환할 수 있다.
3. 생성자와 DTO 사용
스칼라 프로젝션들을 속성으로 가지는 DTO를 생성하고, 해당 DTO 타입으로 데이터 조회 결과를 반환 받을 수 있다.
em3.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
- new 패키지.DTO(스칼라 프로젝션, ... )을 통해 해당 스칼라 프로젝션을 파라미터로 사용하여 DTO의 생성자를 호출한다.
- MemberDTO.class로 리턴 타입을 명시하여, MemberDTO 타입으로 데이터 조회 결과를 반환할 수 있다.
해당 스칼라 프로젝션 예시는 스칼라 프로젝션이 여러개일 때의 방법이다.
스칼라 프로젝션이 1개일 때는 Integer.class, String.class 등으로 리턴 타입을 명시하여 결과를 반환할 수 있다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
// 엔티티 프로젝션
try {
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);
em.flush();
em.clear();
// 엔티티 프로젝션으로 가져온 엔티티들은 모두 영속성 컨텍스트로 관리된다.
List<Member> result = em.createQuery("select m from Member m", Member.class)
.getResultList();
Member findMember = result.get(0);
findMember.setAge(20);
// update 쿼리 실행
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
EntityManager em2 = emf.createEntityManager();
EntityTransaction tx2 = em2.getTransaction();
tx2.begin();
// 임베디드 프로젝션
try {
Order order = new Order();
order.setAddress(new Address("city", "street", "zipcode"));
em2.persist(order);
em2.flush();
em2.clear();
// 소속된 엔티티로부터 조회
List<Address> result = em2.createQuery("select o.address from Order o",
Address.class)
.getResultList();
tx2.commit();
} catch (Exception e) {
tx2.rollback();
} finally {
em2.close();
}
EntityManager em3 = emf.createEntityManager();
EntityTransaction tx3 = em3.getTransaction();
tx3.begin();
// 스칼라 프로젝션
try {
// 방법 1
List resultList = em3.createQuery("select m.username, m.age from Member m")
.getResultList();
Object o = resultList.get(0); // 리스트의 첫번째 객체
Object[] result = (Object[]) o; // 객체의 배열화
System.out.println("result[0] = " + result[0]); //배열의 첫번째 값 username
System.out.println("result[1] = " + result[1]); //배열의 두번째 값 age
// 방법 2, 제네릭을 사용하여 타입 캐스팅 과정을 생략할 수 있다.
List<Object[]> resultList2 = em3.createQuery("select m.username, m.age from Member m")
.getResultList();
Object[] result2 = resultList2.get(0); // 리스트의 첫번째 배열
System.out.println("result[0] = " + result2[0]); //배열의 첫번째 값 username
System.out.println("result[1] = " + result2[1]); //배열의 두번째 값 age
// 방법 3, 생성자 + DTO 사용
List<MemberDTO> resultList3 = em3.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
.getResultList();
MemberDTO memberDTO = resultList3.get(0);
System.out.println("memberDTO.getUsername() = " + memberDTO.getUsername());
System.out.println("memberDTO.getAge() = " + memberDTO.getAge());
tx3.commit();
} catch (Exception e) {
tx3.rollback();
} finally {
em3.close();
}
emf.close();
}
}