JPQL

이상훈·2022년 10월 7일
0

Jpa

목록 보기
9/16

김영한님의 인프런 강의 '자바 ORM 표준 JPA 프로그래밍'을 참고했습니다.

이번 글에서 예제로 사용할 도메인 모델이다.


JPQL의 특징

  • JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
  • JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
  • JPQL은 결국 SQL로 변환된다.
  • JPQL은 동적 쿼리를 만들기 어렵다는 단점이 있다. 따라서 QueryDSL도 같이 사용하는 것이 좋다.

기본 문법과 쿼리 API

JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다. EntityManager.persist() 메서드를 사용하면 되므로 INSERT 문은 없다. JPQL에서 UPDATE, DELETE 문은 벌크 연산이라 하는데 뒤에서 다룰 예정이다. SELECT문을 자세히 알아보자.


SELECT 문

SELECT문은 다음과 같이 사용한다.

select m from Member as m where m.age > 18
  • 엔티티와 속성은 대소문자 구분 O (Member, age)

  • JPQL 키워드는 대소문자 구분 X(SELECT, FROM, where)

  • 테이블 이름이 아닌 엔티티 이름 사용(Member)

  • 별칭은 필수(m)(as는 생략 가능)


TypedQuery, Query

  • TypedQuery : 반환 타입이 명확할 때 사용
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class); 
  • Query : 반환 타입이 명확하지 않을 때 사용
Query query = em.createQuery("SELECT m.username, m.age from Member m"); //username은 string, age는 int

결과 조회 API

  • query.getResultList(): 결과가 하나 이상일 때, 리스트 반환

    • 결과가 없으면 빈 리스트 반환
  • query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환

    • 결과가 없으면: javax.persistence.NoResultException
    • 둘 이상이면: javax.persistence.NonUniqueResultException

📌 query.getSingleResult()는 결과가 1개가 아니면 예외가 터지므로 주의해야 한다!


파라미터 바인딩

  • 이름 기준 파라미터
String usernameParam = "User1";

TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m 
		where m.username = :username", Member.class);

query.setParameter("username", usernameParam);
List<Member> resultList = query.getResultList();

참고로 JPQL API는 대부분 매서드 체인 방식으로 설계되어 있어서 다음과 같이 연속해서 작성할 수 있다.

List<Member> members =
em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class)
  .setParameter("username", usernameParam)
  .getResultList();
  • 위치 기준 파라미터
List<Member> members = 
	em.createQuery("SELECT m FROM Member m where m.username
    = ?1", Member.class)
    .setParameter(1, usernameParam)
    .getResultList();

📌 위치 기준 파라미터는 잘 사용하지 않는다. 이름 기준 파라미터를 쓰자!!


프로젝션

프로젝션이란 SELECT 절에 조회할 대상을 지정하는 것을 의미한다.

  • 엔티티 프로젝션
SELECT m FROM Member m		//회원
SELECT m.team FROM Member m		//팀

이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.


  • 임베디드 타입 프로젝션
SELECT m.address FROM Member m

임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.


  • 스칼라 타입 프로젝션
SELECT m.age FROM Member m

숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다.


여러 값 조회

Query 타입으로 조회

프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신에 Query를 사용해야 한다.

Query query = 
	em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();

Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
	Object[] row = (Object[]) iterator.next();
    String username = (String) row[0];
    Integer age = (Integer) row[1];
}

Object[] 타입으로 조회

List<Object[]> resultList = 
	em.createQuery("SELECT m.username, m.age FROM Member m")
      .getResultList();

for (Object[] row : resultList) {
	String username = (String) row[0];
    Integer age = (Integer) row[1];
}

스칼라 타입뿐만 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있다.


new 명령어로 조회

SELECT 다음에 new 명령어를 사용하면 반환받을 클래스를 지정할 수 있는데 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다.

TypedQuery<UserDTO> query =
	em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age)
    FROM Member m", UserDTO.class);
    
List<UserDTO> resultList = query.getResultList();
  • UserDTO
public class UserDTO {

	private String username;
    private int age;
    
    public UserDTO(String username, int age) {
    	this.username = username;
        this.age = age;
    }
    //..
}

다음 2가지를 주의하자.
• 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
• 순서와 타입이 일치하는 생성자 필요하다.

📌 세 가지 방식 중 new 명령어로 조회가 제일 깔끔해서 사용하는 것을 권장!!


페이징 API

JPA는 페이징을 다음 두 API로 추상화했다. 데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언 덕분이다. 설정한 방언 정보에 따라 JPQL이 각각의 데이터베이스별 SQL로 변환된다.

  • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작).
  • setMaxResults(int maxResult) : 조회할 데이터 수.

페이징 API 예시

검색 조건 : 나이 10살
정렬 조건 : 이름으로 내림차순
페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 3건

public List<Member> findByPage(int age, int offset, int limit) {
	return em.createQuery("select m from Member m where m.age = :age order by m.username desc")
			 .setParameter("age", age)
			 .setFirstResult(offset)
			 .setMaxResults(limit)
			 .getResultList();
}

조건식

  • 기본 CASE 식
select
	case when m.age <= 10 then '학생요금'
		 when m.age >= 60 then '경로요금'
		 else '일반요금'
	end
from Member m
  • 단순 CASE 식
select
		case t.name 
		when '팀A' then '인센티브110%'
		when '팀B' then '인센티브120%'
		else '인센티브105%'
	end
from Team t
  • COALESCE : 하나씩 조회해서 null이 아니면 반환
    ex) 사용자 이름이 없으면 이름 없는 회원을 반환
	select coalesce(m.username,'이름 없는 회원') from Member m
  • NULLIF : 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
    ex) 사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환
	select NULLIF(m.username, '관리자') from Member m

집합과 정렬

함수설명
COUNT결과 수를 구한다.
MAX, MIN최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용한다.
AVG평균값을 구한다.
SUM합을 구한다.
GROUP BY특정 그룹끼리 묶어준다.
HAVINGGROUP BY와 함께 사용하는데 GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링한다.
ORDER BYORDER BY는 결과를 정렬할 때 사용한다.

조인

JPQL도 조인을 지원하는데 SQL 조인과 기능은 같고 문법만 약간 다르다.

  • 내부 조인
SELECT m FROM Member m [INNER] JOIN m.team t
  • 외부 조인
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
  • 세타 조인
select count(m) from Member m, Team t where m.username = t.name

조인 - ON절

JPA 2.1 부터 조인할 때 ON 절을 지원한다. ON절을 활용하면 조인 대상 필터링과 연관관계 없는 엔티티 외부 조인을 할 수 있다.

  • 조인 대상 필터링
    예) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
	SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A' 
  • 연관관계 없는 엔티티 외부 조인
    예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
	SELECT m, t FROM
	Member m LEFT JOIN Team t on m.username = t.name

서브 쿼리

서브 쿼리 지원 함수

  • [NOT] EXISTS (subquery) : 서브 쿼리에 결과가 존재하면 참
  • {ALL | ANY | SOME} (subquery)
    • ALL : 모두 만족하면 참
    • ANY, SOME : 같은 의미, 조건을 하나라도 만족하면 참
  • [NOT] IN (subquery) : 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참

ex) 나이가 평균보다 많은 회원

select m from Member m
where m.age > (select avg(m2.age) from Member m2)

ex) 팀A 소속인 회원

select m from Member m
where exists (select t from m.team t where t.name = ‘팀A')
  • JPQL 서브 쿼리 한계
    JPQL에서는 서브 쿼리를 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용할 수 없다. 참고로 하이버네이트에서는 SELECT 절도 가능하다.

    서브 쿼리로 해결이 안되면 조인으로 생각해보자.


경로 표현식

경로 표현식이란 .(점)을 찍어 객체 그래프를 탐색하는 것이다.


경로 표현식의 용어

  • 상태 필드(state field): 단순히 값을 저장하기 위한 필드(ex: m.username)

  • 연관 필드(association field): 연관관계를 위한 필드

    • 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team)
    • 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)

상태 필드 경로

경로 탐색의 끝, 탐색X

JPQL: select m.username, m.age from Member m 
SQL: select m.username, m.age from Member m

단일 값 연관 경로

묵시적 내부 조인(inner join) 발생, 탐색O

JPQL: select o.member from Order o
SQL: select m.* from Orders o inner join Member m on o.member_id = m.id

JPQL을 보면 o.member를 통해 주문에서 회원으로 단일 값 연관 필드로 경로 탐색을 했다. 단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이것을 묵시적 조인이라 한다. 참고로 묵시적 조인은 모두 내부 조인이다. 외부 조인은 명시적으로 JOIN 키워드를 사용해야 한다.

  • 명시적 조인 : JOIN을 직접 적어주는 것.
    ex) SELECT m FROM Member m JOIN m.team t

  • 묵시적 조인 : 경로 표현식에 의해 묵시적으로 조인이 일어나는 것. 내부 조인 (INNER JOIN)만 할 수 있다.
    ex) SELECT m.team FROM Member m


컬렉션 값 연관 경로

묵시적 내부 조인 발생, 탐색X

JPQL을 다루면서 많이 하는 실수 중 하나는 컬렉션 값에서 경로 탐색을 시도하는 것이다.

select t.members from Team t	//성공
select t.members.username from Team t	//실패

t.members처럼 컬렉션까지는 경로 탐색이 가능하지만 t.members.username처럼 컬렉션에서 경로 탐색을 시작하는 것은 허락하지 않는다. t.members는 멤버 객체가 아니라 컬렉션 그 자체이기 때문이다. 만약 컬렉션에서 경로 탐색을 하고 싶으면 명시적 조인을 사용해서 별칭을 획득해야 한다.

select m.username from Team t join t.members m

결론

묵시적 조인은 SQL 튜닝을 어렵게 만들고 조인이 일어나는 상황을 한눈에 파악하기 어렵다. 따라서 가급적 묵시적 조인 대신에 명시적 조인을 사용하자.


다형성 쿼리

TYPE

TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용한다.

예) Item 중에 Book, Movie를 조회해라

  • JPQL
	select i from Item i
	where type(i) IN (Book, Movie) 
  • SQL
	select i from i
	where i.DTYPE in (‘B’, ‘M’)

THREAT

Threat는 JPA 2.1에 추가된 기능인데 자바의 타입 캐스팅과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. FROM, WHERE, SELECT(하이버네이트 지원)에서 사용할 수 있다.

예) 부모인 Item과 자식 Book이 있다.

  • JPQL
select i from Item i
where treat(i as Book).auther = ‘kim’ 
  • SQL
select i.* from Item i
where i.DTYPE = ‘B’ and i.auther = ‘kim’

엔티티 직접 사용

기본 키 값

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.

  • JPQL
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용 
  • SQL (JPQL 둘다 아래 SQL 실행)
select count(m.id) as cnt from Member m

외래 키 값

  • JPQL
Team team = em.find(Team.class, 1L); 

String qlString =select m from Member m where m.team = :team”; 
List resultList = em.createQuery(qlString) 
					.setParameter("team", team) 	//엔티티를 직접 사용
 					.getResultList(); 
String qlString =select m from Member m where m.team.id = :teamId”; 
List resultList = em.createQuery(qlString) 
 					.setParameter("teamId", teamId)		//외래 키에 식별자를 직접 사용
 					.getResultList(); 
  • SQL (JPQL 둘다 아래 SQL 실행)
select m.* from Member m where m.team_id=?

Named 쿼리

JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있다.

  • 동적 쿼리 : em.createQuery("select ...") 처럼 JPQL을 문자로 완성해서 직접 넘기는 것을 동적 쿼리라 한다. 런타임에 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다.

  • 정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을 Named 쿼리라 한다. Named 쿼리는 한 번 정의하면 변경할 수 없는 정적인 쿼리다.

Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해둔다. 따라서 오류를 빨리 확인할 수 있고 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다. 그리고 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스의 조회 성능 최적화에도 도움이 된다. Named 쿼리는 @NamedQuery 어노테이션을 사용해서 자바 코드에 작성하거나 또는 XML 문서에 작성할 수 있다.


어노테이션

  • Named 쿼리를 어노테이션에 정의
@Entity
@NamedQuery(
	name = "Member.findByUsername",
	query="select m from Member m where m.username = :username")
public class Member {
	...
}
  • @NamedQuery 사용
List<Member> resultList = 
 em.createNamedQuery("Member.findByUsername", Member.class)
		.setParameter("username", "회원1")
 		.getResultList();

XML

  • XML이 항상 우선권을 가진다.
  • 애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.

벌크 연산

if 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?

  • JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행
      1. 재고가 10개 미만인 상품을 리스트로 조회한다.
      1. 상품 엔티티의 가격을 10% 증가한다.
      1. 트랜잭션 커밋 시점에 변경 감지가 동작한다.

따라서 변경된 데이터가 100건이라면 100번의 UPDATE SQL이 실행된다...


하지만 벌크 연산은 쿼리 한 번으로 여러 테이블의 로우를 변경할 수 있다.

String qlString = "update Product p " +
				  "set p.price = p.price * 1.1 " + 
                  "where p.stockAmount < :stockAmount"; 

int resultCount = em.createQuery(qlString) 
                    .setParameter("stockAmount", 10) 
                    .executeUpdate(); 

벌크 연산 주의점

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다. 따라서 다음과 같은 해결책이 있다.

  • 벌크 연산을 먼저 실행
  • 벌크 연산 수행 후 영속성 컨텍스트 초기화
profile
Problem Solving과 기술적 의사결정을 중요시합니다.

0개의 댓글