자바 ORM 표준 JPA 프로그래밍 스터디 - 5주차 (JPQL)

큰모래·2023년 6월 4일
0

10.2 - JPQL

10.2.1 기본 문법과 쿼리 API


SELECT 문


SELECT m FROM Member AS m where m.username = Hello``

  • 엔티티와 속성은 대소문자를 구분한다. (Member, username)
  • SELECT, FROM AS 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
  • JPQL 에서 사용한 Member는 엔티티 명이다. (엔티티 명을 지정하지 않으면 클래스명을 기본값으로 사용)
  • Member AS m 같은 별칭은 필수. (지정하지 않으면 잘못된 문법)

TypeQuery, Query


TypeQuery

  • 반환 타입을 명확하게 지정할 수 있으면 TypeQuery 사용
  • 조회 대상이 Member 엔티티로 명확하다.
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

List<Member> resultList = query.getResultList();
for (Member member : resultList) {
		System.out.println("member = " + member);
}

Query

  • 반환 타입을 명확하게 지정할 수 없다.
  • 조회 대상이 m.username, m.age 두개임
  • 조회 대상이 둘 이상이면 Object[] 를 반환, 한 개이면 Object를 반환한다.
Query query = em.createQuery("SELECT m.username, m.age from Member m");

List resultList = query.getResultList();
for (Object o : resultList) {
		Object[] result = (Object[]) o; //결과가 둘 이상이라 배열
		System.out.println("username = " + result[0]);
		System.out.println("age = " + result[1]);
}

결과조회


  • query.getResultList()
    • 결과를 List 컬렉션으로 반환, 없으면 빈 컬렉션을 반환
  • query.getSingleResult()
    • 결과가 정확히 하나일 때 사용한다.
    • 결과가 없으면 NoResultException 예외가 발생한다.
    • 결과가 1개보다 많으면 NonUniqueResultException 예외가 발생한다.

10.2.2 파라미터 바인딩


이름 기준 파라미터

  • 파라미터를 이름으로 구분하는 방법
  • 이름 기준 파라미터는 앞에 :를 사용한다.
  • :username으로 이름 기준 파라미터를 정의한다.
  • query.setParameter()에서 파라미터를 바인딩한다.
String usernameParam = "User1";

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

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

//체인 방식
List<Member> members = 
		em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class)
		.setParameter("username", usernameParam)
		.getResultList();

위치 기준 파라미터

  • ?다음에 위치 값을 준다. 위치 값은 1부터 시작
  • 위치 기준 보다는 이름 기준을 사용하는게 더 명확하다.

List<Member> members = 
		em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class)
		.setParameter(1, usernameParam)
		.getResultList();

10.2.3 프로젝션


엔티티 프로젝션

  • 원하는 객체를 바로 조회
  • 조회된 엔티티는 영속성 컨텍스트에서 관리된다.
SELECT m FROM Member m       //회원
SELECT m.team FROM Member m  //팀

임베디드 타입 프로젝션

  • 엔티티를 통해서 임베디드 타입 조회 가능 (임베디드 타입이 시작점이 되면 안됨)
//잘못된 쿼리
String query = "SELECT a FROM Address a";

//정상적인 쿼리
String query = "SELECT o.address FROM Order o";
List<Address> addresses = em.createQuery(query, Address.class).getResultList();

스칼라 타입 프로젝션

  • 기본 데이터 타입들을 스칼라 타입이라 한다.
List<String> usernames = 
		em.createQuery("SELECT username FROM Member m", String.class)
			.getResultList();

Double orderAmountAvg = 
		em.createQuery("SELECT AVG(o.orderAmount) FROM Order o", Double.class)
			.getSingleResult()

여러 값 조회

  • 제네릭에 Object 배열을 사용하면 조금 더 간결하게 사용할 수 있다.
List<Object[]> resultList = 
		em.createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o")
				.getResultList();

for (Object[] row : resultList) {
		Member member = (Member) row[0];     //엔티티
		Product product = (Product) row[1];  //엔티티
		int orderAmount = (Integer) row[2];  //스칼라
}

New 명령어

  • 실무에서는 Object[] 배열을 사용하지 않고 Dto 객체를 만들어서 사용한다.
  • New 명령어를 통해 불필요한 객체 변환 작업을 제거할 수 있다.
  • TypeQuery로 명확하게 받을 수 있어서 객체 변환 작업을 줄일 수 있다.
  • 패키지 명을 포함한 전체 클래스 명을 입력해야 한다. (jpabook.jpql.UserDTO)
  • 순서와 타입이 일치하는 생성자가 필요하다. (UserDTO(m.username, m.age))
public class UserDTO {
		private String username;
		private int age;
		
		public UserDTO(String username, int age) {
				this.username = username;
				this.age = age;
		}
		...
}

//New 명령어 사용 전
List<Object[]> resultList = 
		em.createQuery("SELECT m.username, m.age FROM Member m")
				.getResultList();

List<UserDTO> userDTOs = new ArrayList<UserDTO>();
for (Object[] row : resultList) {
		UserDTO userDTO = new UserDTO((String)row[0], (Integer)row[1]);
		userDTOs.add(userDTO);
}
return userDTOs;

//New 명령어 사용 후
TypeQuery<UserDTO> query = 
		em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m");

List<UserDTO> resultList = query.getResultList();
return resultList;

10.2.4 페이징 API


JPA는 페이징 처리를 두 API로 추상화했다.

  • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
  • 데이터베이스 방언(dialect) 덕분에 같은 api로 처리가 가능하다.
TypeQuery<Member> query = 
		em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);

query.setFirstResult(10);
query.setMaxResults(20);
query.getResultList();

10.2.5 집합과 정렬


select
		COUNT(m),   //회원수
		SUM(m.age), //나이 합  
		AVG(m.age), //평균 나이
		MAX(m.age), //최대 나이
		MIN(m.age)  //최소 나이
from Member m

집합 함수 사용 시 참고사항

  • null 값은 무시되므로 통계에 잡히지 않는다.
  • 값이 없는데 sum, avg, max, min 함수를 사용하면 null, count는 0

GROUP BY, HAVING

  • GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다.
    SELECT t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
    FROM Member m
    LEFT JOIN m.team t
    GROUP BY t.name
  • HAVINGGROUP BY와 함께 사용되며, 그룹화한 통계 데이터를 기준으로 필터링한다.
    SELECT t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
    FROM Member m
    LEFT JOIN m.team t
    GROUP BY t.name
    HAVING AVG(m.age) >= 10

이런 쿼리들을 보통 리포팅 쿼리 혹은 통계 쿼리라고 한다.

통계 쿼리는 전체 데이터를 기준으로 처리하므로 실시간으로 사용하기엔 부담이 많다.

정렬(ORDER BY)

select m from Member m order by m.age DESC, m.username ASC

10.2.6 JPQL 조인


내부 조인

  • INNER는 생략 가능
  • Member m INNER JOIN m.team : 연관된 필드로 조인을 한다.
String teamName = "팀A";
String query = "SELECT m FROM Member m INNER JOIN m.team t "
								+ "where t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
		.setParameter("teamName", teamName)
		.getResultList();

//JPQL
SELECT m
FROM Member m INNER JOIN m.team t
where t.name = '팀A';

//SQL
SELECT 
		M.ID AS ID,
		M.AGE AS AGE,
		M.TEAM_ID AS TEAM_ID,
		M.NAME AS NAME
FROM 
		MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID
WHERE 
		T.NAME = ?

외부 조인

//JPQL
SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t

//SQL
SELECT 
		M.ID AS ID,
		M.AGE AS AGE,
		M.TEAM_ID AS TEAM_ID,
		M.NAME AS NAME
FROM 
		MEMBER M LEFT OUTER JOIN TEAM T ON M.TEAM_ID = T.ID
WHERE 
		T.NAME = ?

컬렉션 조인

  • 팀 → 회원은 일대다 조인이면서 컬렉션 값 연관 필드(t.members)를 사용한다.
  • 내부 조인 시 조회 안되는 문제가 발생할 수 있으므로 외부 조인 사용
    • 내부 조인을 사용하면 만약, 팀에 회원이 없을 경우 팀도 조회가 되지 않는 문제가 발생한다.
SELECT t, m From Team t LEFT JOIN t.members m

10.2.7 페치 조인


JPQL에서 성능 최적화를 위해 제공하는 기능이다.

연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이며 join fetch 명령어를 통해 사용한다.

엔티티 페치 조인

  • 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회
  • 회원과 팀 객체가 객체 그래프를 유지하면서 조회된다.
  • member.getTeam().name()을 사용해도 지연 로딩 발생 안 함(프록시 객체X)
//JPQL
select m
from Member m join fetch m.team

//실제 실행된 SQL
SELECT M.*, T.*
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID

컬렉션 페치 조인

  • 팀 엔티티를 조회하면서 연관된 회원 컬렉션도 함께 조회
  • 페치 조인 결과로 팀 A가 2개가 생김
//JPQL
select t
from Team t join fetch t.members
where t.name = '팀A'

//실제 실행된 SQL
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

페치 조인과 DISTINCT

  • DISTINCT를 추가하고, 애플리케이션에서 한번 더 중복을 제거
    //JPQL
    select distinct t
    from Team t join fetch t.members
    where t.name = '팀A'
    
    //애플리케이션 출력 결과
    teamname =A, team = Team@0x100
    ->username = 회원1, member = Member@0x200
    ->username = 회원2, member = Member@0x200
    • SQL에서는 DISTINCT를 추가하여도 데이터가 다르므로 효과가 없음
    • 애플리케이션에서는 팀A가 중복으로 2개 있는걸 확인하고 중복된 데이터를 제거하므로 팀A가 하나만
      조회된다.

페치 조인과 일반 조인의 차이

  • JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다.
  • 단지 SELECT 절에서 지정한 엔티티만 조회할 뿐이다.
  • 반면에 fetch join을 사용하면 연관된 엔티티도 함께 조회한다.
//내부 조인 JPQL
select t
from Team t join t.member m
where t.name = '팀A'

//실행된 SQL
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

//페치 조인의 경우
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

페치 조인의 특징과 한계

  • SQL 한 번으로 연관된 엔티티를 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다.
  • 글로벌 로딩 전략을 지연 로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치 조인을 적용한다.
  • 글로벌 로딩 전략은 될 수 있으면 지연 로딩, 최적화가 필요하면 페치 조인을 사용한는 것이 효과적이다.
  • 둘 이상의 컬렉션을 페치할 수 없다.(컬렉션 * 컬렉션으로 조회 개수가 엄청 커질 수 있다)
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
  • 억지로 여러 테이블을 페치 조인해서 사용하는 것 보다는 필요한 필드만 조회해서 dto로 반환하는 것이 효과적이다.

10.2.8 경로 표현식


.을 찍어 객체 그래프를 탐색하는 것

  • 상태 필드 : 단순히 값을 저장하기 위한 필드
    • t.username, t.age
  • 연관 필드 : 연관관계를 위한 필드
    • 단일 값 연관 필드 : m.team
    • 컬렉션 값 연관 필드 : m.orders

경로 표현식과 특징

  • 상태 필드 경로 : 경로 탐색의 끝, 더는 탐색 불가능
  • 단일 값 연관 경로 : 묵시적으로 내부 조인이 일어난다. 계속 탐색 가능
    • o.member를 통해 주문에서 회원으로 단일 값 연관 필드로 경로 탐색

    • 단일 값 연관 필드로 경로 탐색을 하면 내부 조인이 일어나는데 이것을 묵시적 조인이라고 한다.

    • 묵시적 조인은 모두 내부 조인이다.

      //JPQL
      select o.member from Order o
      
      //실행 SQL
      select m.*
      from Orders o
      		inner join Member m on o.member_id=m.id //묵시적 조인 (모두 내부 조인)
  • 컬렉션 값 연관 경로 : 묵시적으로 내부 조인이 일어난다. 더는 탐색 불가능
    • JPQL에서 많이 하는 실수 중 하나는 컬렉션 값에서 경로 탐색을 시도하는 것이다.

    • 컬렉션에서 경로 탐색을 더 하고 싶다면 조인을 사용해서 새로운 별칭을 획득해야 한다.

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

경로 탐색을 사용한 묵시적 조인 시 주의사항

  • 항상 내부 조인이다.
  • 컬렉션은 경로 탐색의 끝이다. 추가적으로 탐색하려면 명시적으로 조인을 사용해서 별칭을 얻어야 한다.
  • 묵시적 조인은 조인이 일어나는 상황을 하눈에 파악하기 어렵다는 단점이 있다.
  • 성능이 중요하다면 분석하기 쉽도록 명시적 조인을 사용하는게 좋다.

10.2.9 서브 쿼리


서브 쿼리는 WHERE, HAVING 절에서만 사용할 수 있다.

//나이가 평균보다 많은 회원 조회
select m from Member m
where m.age > (select avg(m2.age) from Member m2)

서브 쿼리 함수

  • EXISTS
    • 서브쿼리에 결과가 존재하면 참이다. NOT은 반대

    • 팀A 소속인 회원

      select m from Member m
      where exists (select t from m.team t where t.name = '팀A')
  • ALL | ANY | SOME
    • ALL : 조건을 모두 만족하면 참
    • ANY or SOME : 조건을 하나라도 만족하면 참
  • IN
    • 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참이다.

    • 20세 이상을 보유한 팀

      select t from Team t
      where t IN (select t2 from Team t2 JOIN t2.members m2 where m2.age >= 20)

10.2.10 조건식


컬렉션 식

  • 컬렉션은 컬렉션 식만 사용 가능 (is null 불가능)
  • is (not) empty
  • (not) member (of)
//주문이 하나라도 잇는 회원 조회
select m from Member m
whehre m.orders is not empty

//memberparam이 컬렉션에 포함되어 있는지 확인
select t from Team t
where :memberparam member of t.members

CASE 식

  • 특정 조건에 따라 분기 할 때 CASE 식을 사용
  • 기본 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

10.2.11 다형성 쿼리


JPQL로 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회한다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {...}

@Entity 
@DiscriminatorValue("B")
public class Book extends Item {
		...
		private String author;
}

//Album, Movie 생략

List resultList = em.createQuery("select i from Item i").getResultList();

//단일 테이블 전략(InheritanceType.SINGLE_TABLE) 을 사용한 경우
SELECT * FROM ITEM

//조인 전략(InheritanceType.JOINED)을 사용한 경우
SELECT
		I.ITEM_ID, ...,
		B.AUTHOR, ...,
		A.ARTIST, ...,
		M.ACTOR, ...,
FROM ITEM I
LEFT OUTER JOIN
		BOOK B ON I.ITEM_ID=B.ITEM_ID
LEFT OUTER JOIN
		ALBUM A ON I.IOTEM_ID=A.ITEM_ID
LEFT OUTER JOIN
		MOVIE M ON I.ITEM_ID=M.ITEM_ID

TYPE

조회 대상을 특정 자식 타입으로 한정할 때 사용한다.

//JPQL
select i from Item i
where type(i) IN (Book, Movie)

//SQL
SELECT I FROM ITEM I
WHERE I.DTYPE IN ('B', 'M')

10.2.13 기타 정리


  • enum은 = 비교 연산만 지원한다.
  • 임베디드 타입은 비교를 지원하지 않는다.

EMPTY STRING

  • JPA표준은 ''을 길이 0인 Empty String으로 정의 하지만 DB에 따라 ''을 NULL로 사용하는 데이터베이스도 있으므로 확인하고 사용해야 한다.

NULL 정의

  • 조건을 만족하는 데이터가 하나도 없으면 NULL
  • NULL과의 모든 수학적 계산 결과는 NULL

10.2.14 엔티티 직접 사용


기본 키 값

  • JPQL에서 엔티티 객체를 직접 사용하면 SQL로 변환되면서 해당 엔티티의 기본 키 값을 사용한다.
//JPQL
select m from Member m where m = :member

//SQL
select m.*
from Member m
where m.id = ?

외래 키 값

//JPQL
select m from Member m where m.team = :team

//SQL
select m.*
from Member m
where m.team_id = ?

10.2.15 Named 쿼리: 정적 쿼리


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

  • 동적 쿼리 : em.createQuery("select ....") 처럼 런타임에 특정 조건에 따라 JPQL을 동적으로 구성
  • 정적 쿼리 : 미리 정의한 쿼리에 이름을 부여하여 사용, 한번 정의하면 변경 못함 (Named 쿼리)
    • 로딩 시점에 JPQL문법을 확인해 오류 확인이 편리
    • 재사용하므로 성능상도 이점
    • @NamedQuery 어노테이션을 사용하거나 XML 문서에 작성하여 사용
@Entity
@NamedQuery(
		name = "Member.findByUsername",
		query = "select m from Member m where m.username = :username")
public class Member {
		...
}

//NamedQuery 사용
List<Member> memberList = em.createNamedQuery("Member.findByUsername",
		Member.class)
				.setParametmer("username", "회원1")
				.getResultList();
profile
큰모래

0개의 댓글