JPQL은 JPA 에서 엔터티 객체를 조회하기 위한 쿼리 언어다. 관계형 데이터베이스의 SQL과 유사하지만, 객체 지향적 특성을 가진 쿼리 언어다.
JPQL의 주요 목적은 데이터베이스 테이블이 아닌 엔터티 객체를 대상으로 쿼리를 수행하는 것이다.
쿼리 API
TypedQuery, Query
- TypedQuery: 반환 형식이 명확한 경우 사용. 반환될 엔티티나 값의 자료형을 지정할 수 있음.
- Query: 반환 형식이 명확하지 않은 경우 사용. 주로 다양한 형식의 값을 한 번에 반환할 때 사용.
JPQL의 쿼리는 TypedQuery 와 Query 가 존재한다. 반환형이 존재하면 TypedQuery 그렇지 않으면 Query이다.
타입쿼리는 반환형의 클레스가 명확할 때 적용한다.
query1은 Member라는 반환형이 명확하니 Member.class를 추가하였고,
query2도 반환형이 m.userName이라는 문자열이므로 String.class를 반환한다.
query3는 숫자, 문자열 모두 포함하므로 반환형이 명확하지 않다. 따라서 반환 클레스를 명시하지 않는다.
즉, TypedQuery가 아닌 Query이다.
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 query3 = em.createQuery("select m.userName, m.age from Member m");
결과 조회 API
- query.getResultList() : 결과가 하나 이상일 때, 리스트 반환
- 결과가 없으면 빈 리스트 반환
- query.getSingleResult() : 결과가 정확히 하나일 때
- 결과가 없으면 : javax.persistence.NoResultException
- 둘 이상이면 : javax.persistence.NonUniqueResultException
Member member = new Member();
member.setUserName("member1");
member.setAge(10);
em.persist(member);
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
List<Member> resultList = query1.getResultList();
for (Member member1 : resultList) {
System.out.println("member1.getUserName() = " + member1.getUserName());
}
Member singleResult = query1.getSingleResult();
System.out.println("singleResult.getUserName() = " + singleResult.getUserName());
EnttiyManager.createQuery() 를 그냥 반환하면 TypedQuery 혹은 Query가 나오기 때문에 이 쿼리 타입들에서 한번더 값을 조회 해야 한다.
다중 객체를 조회하는 getResultList(), 단일 객체를 조회하는 getSingleResult()를 사용하면 원하는 엔티티의 타입을 얻을 수 있다.
null이 나와도 예외없이 반환하는 getResultList와는 달리, getSingleResult는 값이 null이라면 NoResultException이 발생하고, 둘 이상이라면 NonUniqueResultException이 발생한다.
Spring Data JPA에서는 단일 반환일 때 Optional을 반환하여 예외가 터지지 않게 한다.
파라미터 바인딩 - 이름 기준, 위치 기준
JPQL에 원하는 파라미터를 넣을때에는 이름을 기준으로 넣는것과, 순서를 기준으로 넣는방법이 있다.
이름기준
where문에 =: 파라미터명 을 추가한다.
이후 setParameter에서 기존에 입력한 파라미터명과, 실제 데이터를 넣는다.
Member singleResult = em.createQuery("select m from Member m where m.userName = :username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
System.out.println("singleResult = " + singleResult.getUserName());
순서기준 (비추천, 순서가 밀릴 위험)
where 절의 = : 파라미터
대신 = ?숫자 로 변경하면 된다.
밑의 파라미터도 문자열대신 숫자로 변경
다만, 중간에 값이 추가되면 값이 그대로 밀릴 수 있으므로 권장하지 않는다.
Member singleResult = em.createQuery("select m from Member m where m.userName = ?1", Member.class)
.setParameter(1, "member1")
.getSingleResult();
프로젝션(SELECT)
- SELECT 절에 조회할 대상을 지정하는 것
- 프로젝션 대상 : 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)
- SELECT m FROM Member m -> 엔티티 프로젝션
- SELECT m.team FROM Member m -> 엔티티 프로젝션
- SELECT m.address From Member m -> 임베디드 타입 프로젝션
- SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
관계형 데이터베이스의 경우에는 스칼라 타입만 조회가 가능한데, JPQL의 경우 엔티티, 임베디드 타입도 조회 가능하다.
스칼라 타입이란, 우리가 일반적으로 DB에서 데이터를 조회할 때 반환되는 기본형 값들을 말한다.
엔티티 프로젝션
엔티티 프로젝션을 하면 10개든 20개든 엔티티들이 전부 영속성 컨텍스트에서 관리된다.
다음과 같이 age가 10인 member를 영속한 후, 쿼리를 날리고 캐시를 비웠다.
Member member = new Member();
member.setUserName("member1");
member.setAge(10);
em.persist(member);
em.flush();
em.clear();
List<Member> resultList = em.createQuery("select m from Member m", Member.class)
.getResultList();
Member findMember = resultList.get(0);
findMember.setAge(20);
엔티티 프로젝션으로 member를 조회하면, 수정을 했을 때 JPA에서는 변경을 감지하고 update 쿼리를 실행한다
Hibernate:
/* update
for com.example.jpa.jpql.Member */update member
set
age=?,
team_id=?,
user_name=?
where
id=?
엔티티 프로젝션(참조 엔티티의 경우)
참조 엔티티를 조회할 때에는 당연하지만 반환 클레스도 참조 클레스로 변경해야한다.
첫번째 방식은 join 유무가 한눈에 파악하기 어려우므로
두번째 방식처럼 명시적으로 join을 표시해주는것이 좋다.
List<Team> worstResult = em.createQuery("select m.team from Member m", Team.class)
.getResultList();
List<Team> bestResult = em.createQuery("select t from Member m join m.team t", Team.class)
.getResultList();
참조 엔티티를 반환형으로 지정했지만 쿼리 자체는 멤버에서 찾으므로 join 쿼리가 나간다.
Hibernate:
/* select
m.team
from
Member m */ select
t1_0.id,
t1_0.name
from
member m1_0
join
team t1_0
on t1_0.id=m1_0.team_id
임베디드 타입 프로젝션
임베디드 타입은 엔티티가 아닌 값이기 때문에 select의 주체가 되지 못한다.
즉, Entity.EmbeddedType 으로 조회해야 한다.
그렇기에 두번째 코드는 컴파일 에러가 나타난다.
스칼라 타입 프로젝션
반환타입을 제거하면 된다.
List resultList = em.createQuery("select m.userName, m.age from Member m")
.getResultList();
이 스칼라 타입은 어떻게 조회될까?
SELECT m.username, m.age FROM Member m
1. Query 타입으로 조회
아래 코드는 반환타입 따로 지정하지 않아 반환타입 없이 그냥 List로만 반환되지만,
실제로는 내부적으로 Object[] 형식을 취한다고 한다.
실제로 조회해보면 다음과 같다.
Member member = new Member();
member.setUserName("member1");
member.setAge(10);
em.persist(member);
List resultList = em.createQuery("select m.userName, m.age from Member m")
.getResultList();
for (Object o : resultList) {
log.info("o = {}",o);
log.info("o.getClass = {}",o.getClass());
}
class [Ljava.lang.Object; 이 Object[] 를 의미한다.
o = [member1, 10]
o.getClass = class [Ljava.lang.Object;
2. Object[] 타입으로 조회
Member member = new Member();
member.setUserName("member1");
member.setAge(10);
em.persist(member);
List<Object[]> result = em.createQuery("select m.userName, m.age from Member m")
.getResultList();
for (Object[] objects : result) {
log.info("objects={}", Arrays.toString(objects));
}
3. new 명령어로 조회
- 단순 값을 DTO로 바로 조회
- SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
- 패키지명을 포함한 전체 클래스명 입력
- 순서와 타입이 일치하는 생성자 필요
memberDTO 추가, toString을 오버라이딩 한다.
public class MemberDTO {
private String username;
private int age;
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
@Override
public String toString() {
return "MemberDTO{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
}
이후 조회문에서 DTO의 패키지 주소를 전부 적어서 mebmerDTO 객체를 추가한다.
Member member = new Member();
member.setUserName("member1");
member.setAge(10);
em.persist(member);
List<MemberDTO> resultList = em.createQuery("select new com.example.jpa.jpql.MemberDTO(m.userName, m.age) from Member m", MemberDTO.class)
.getResultList();
for (MemberDTO memberDTO : resultList) {
log.info(memberDTO.toString());
}
다음처럼 DTO에 감싸져서 온다.
MemberDTO{username='member1', age=10}
마지막 방법이 제일 좋아보이지만 DTO로 조회하려면 패키지명을 전부 적어야하는 불편함이 있다. 문자열로 조회를 하기때문에 오타를 찾기도 어렵다.
하지만 이는 QueryDSL로 대처가 가능하다.