-
Notifications
You must be signed in to change notification settings - Fork 0
JPQL
- JPQL 문법
- 집합과 정렬
- TypeQuery, Query
- 결과 조회 API
- 파라미터 바인딩
- 프로젝션
- JPQL 기본 함수
- 경로 표현식
- Fetch join
- 다형성 쿼리
- 엔티티 직접 사용
- Named 쿼리
- 벌크 연산
-
엔티티와 속성은 대소문자 구분한다.
ex) Member, age
select m from **Member** as m where m.**age** > 18
-
JPQL 키워드(select, from, where 등)는 대소문자 구분 X
-
테이블의 이름이 아니라, 엔티티 이름(클래스 이름으로 default)을 사용한다.
-
@Entity(name = "")을 통해서 엔티티 이름을 변경해줄수도 있다.
-
-
별칭(m)은 필수로 입력해야 한다.
-
as는 생략 가능하지만, 표준 스펙에 맞춰 사용하는 것이 좋다.
select_문 :: = select_절 from_절 [where_절] [groupby_절] [having_절] [orderby_절] update_문 :: = update_절 [where_절] delete_문 :: = delete_절 [where_절]
-
-
표준 SQL 문법과 거의 비슷하다고 보면 된다.
-
COUNT: 데이터 개수 -
SUM: 속성 값의 합 -
AVG: 속성 값의 평균 -
MAX: 속성 값의 최대 -
MIN: 속성 값의 최소 -
GROUP BY: 그룹 지정-
HAVING: 그룹에 속할 조건 지정
-
-
ORDER BY: 정렬 기준select COUNT(m), //회원수 SUM(m.age), //나이 합 AVG(m.age), //평균 나이 MAX(m.age), //최대 나이 MIN(m.age) //최소 나이 from Member m
-
TypeQuery: 반환 타입이 명확할 때 사용- 제네릭을 통해 가능하다.
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: 문자열,age: 숫자 타입 두 개를 받아와 특정 타입 하나를 지정을 해줄 수가 없기 때문에Query를 사용했다.- 예를 들어
username만 받아오게 한다면 ,TypedQuery<String> query = em.createQuery("SELECT m.username FROM Member m", String.class);으로 해줄 수 있다.
- 예를 들어
-
- 결과가 하나 이상일 때, 결과가 담긴 리스트 반환
-
결과가 없으면, 빈 리스트 반환
-
NullPointerException을 걱정할 필요가 없다.
-
- 결과가 정확히 하나일 경우에만, 단일 객체 반환
-
결과가 없으면:
javax.persistence.NoResultException -
둘 이상이면:
javax.persistence.NonUniqueResultException -
Spring DATA JPA에서는 결과가 없을 경우Null또는Optional을 반환하여 예외를 일으키지 않게 되어있다. 그러므로Spring DATA JPA사용할 때와 헷갈리지 말아야한다.
-
":파라미터 이름" 사용
Member member = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class); .setParameter("username", "member1"); .getSingleResult();
-
"?파라미터 위치" 사용
-
위치 기준은 순서가 바뀌거나 할 경우, 에러가 날 상황이 생길 수 있기 때문에이름 기준을 사용하는 것이 좋다.
Member member = em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class); .setParameter(1, "member1"); .getSingleResult();
-
-
SELECT 절에 조회할 대상을 지정하는 것
- 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)
SELECT m FROM Member m -> 엔티티 프로젝션 (Member entity) SELECT m.team FROM Member m -> 엔티티 프로젝션 (Team entity) SELECT o.address FROM Order o -> 임베디드 타입 프로젝션 SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
- 앞에
DISTINCT를 추가하여 중복 제거도 가능하다.
-
아래의 List<
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);
-
findMember.setAge(20);를 실행 시, UPDATE 쿼리 문이 실행되어 DB에 반영이 된다.- 즉, 엔티티 프로젝션
em.createQuery("select m from Member m", Member.class)을 통해 가져오는 모든Member는 영속성 컨텍스트에 의해 관리가 된다고 보면 된다.
- 즉, 엔티티 프로젝션
-
-
아래의 코드는 어떤 쿼리를 날리게 될까?
. List<Member> result = em.createQuery("select m.team from Member m", Team.class) .getResultList();
-
inner join을 통해Team을 찾아온다.-
"select m.team from Member m"처럼 심플하게 JPQL를 작성했지만, 실제는Join을 통한 더 복잡한 쿼리가 만들어진다.-
직접
join 문법을 추가해주지 않아도 알아서 추가해주기 때문에 더 편하다고 느껴질 수 있으나,join은 성능에 영향을 끼칠 수 있고 이러한 부분을 모르고 넘어갈 수 있기 때문에, 결과적으로 똑같은 SQL 쿼리가 생성된다고 하더라도 아래와 같이 실제 SQL 쿼리처럼 JPQL에join 문법을 직접 추가해주는 것이 더 좋다.List<Member> result = em.createQuery("select t from Member m join m.team t", Team.class) .getResultList();
-
-
-
-
아래의 코드는 어떤 쿼리를 날리게 될까?
em.createQuery("select o.address from Order o", Order.class) .getResultList();
- Address 관련된(city, street, zipcode) 정보를 조회하는 쿼리를 잘 생성하는 것을 볼 수 있다.
- Address가
@Embeddable로 선언된 값 객체이기 때문에select a from Address a처럼 쿼리를 만들수는 없다. 즉, 임베디드 타입 프로젝션은select o.address from Order o처럼 엔티티로부터 시작해야 한다.
- Address가
- Address 관련된(city, street, zipcode) 정보를 조회하는 쿼리를 잘 생성하는 것을 볼 수 있다.
-
아래의 코드는 어떤 쿼리를 날리게 될까?
em.createQuery("select distinct m.username, m.age from Member m", Member.class) .getResultList();
- 스칼라 타입 프로젝션은 일반 SQL의 프로젝션과 똑같다고 보면 된다.
-
Query 타입으로 조회
Member member = new Member(); member.setUsername("member1"); member.setAge(29); em.persist(member); em.flush(); em.clear(); List results = em.createQuery("select m.username, m.age from Member m") .getResultList(); Object o = results.get(0); Object[] result = (Object[]) o; System.out.println("username = " + result[0]); System.out.println("age = " + result[1]); //결과 username = member1 age = 29
-
Object[] 타입으로 조회
List<Object[]> results = em.createQuery("select m.username, m.age from Member m") .getResultList(); Object[] result = results.get(0); System.out.println("username = " + result[0]); System.out.println("age = " + result[1]); //결과 username = member1 age = 29
-
new 명령어로 조회
-
단순 값을 DTO로 바로 조회
- 순서와 타입이 일치하는 생성자 필요
package jpql; public class MemberDTO { private String username; private int age; public MemberDTO(String username, int age) { this.username = username; this.age = age; } public String getUsername() { return username; } public int getAge() { return age; } }
- 패키지 명을 포함한 전체 클래스 명 입력
List<MemberDTO> results = em.createQuery("select **new jpql.MemberDTO(m.username, m.age)** from Member m", **MemberDTO.class**) .getResultList(); MemberDTO memberDTO = results.get(0); System.out.println("username = " + memberDTO.getUsername()); System.out.println("age = " + memberDTO.getAge()); //결과 username = member1 age = 29
-
- 조회 시작 위치
- 조회할 데이터 수
em.createQuery("select m from Member m order by m.age desc", Member.class)
.**setFirstResult(0)**
.**setMaxResults(10)**
.getResultList();
//결과
/* select
m
from
Member m
order by
m.age desc */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as team_id4_0_,
member0_.username as username3_0_
from
Member member0_
order by
member0_.age desc **limit ?**-
왜
maxResult가 적용이 안되었지? 라고 생각할 수 있다. 즉, order by절에offset이 추가가 안되어있고member0_.age desc limit ?인 이유는setFirstResult(0)으로 설정해주었기 때문이다.-
setFirstResult()에 0이 아닌 숫자를 대입하게 되면offset이 포함된다.
List<Member> results = em.createQuery("select m from Member m order by m.age desc", Member.class) .setFirstResult(**1**) .setMaxResults(10) .getResultList(); //결과 /* select m from Member m order by m.age desc */ select member0_.id as id1_0_, member0_.age as age2_0_, member0_.TEAM_ID as team_id4_0_, member0_.username as username3_0_ from Member member0_ order by member0_.age desc limit ? offset ?
-
- 둘 이상의 테이블에 존재하는 공통 속성의 값이 같은 것을 결과로 추출
-
ex) SELECT m FROM Member m [INNER] JOIN m.team t
List<Member> innerJoin = em.createQuery("**select m from Member m inner join m.team t**", Member.class) .getResultList(); // 결과 /* select m from Member m inner join m.team t */ select member0_.id as id1_0_, member0_.age as age2_0_, member0_.TEAM_ID as team_id4_0_, member0_.username as username3_0_ from Member member0_ **inner join** Team team1_ on member0_.TEAM_ID=team1_.id
-
- 왼쪽/오른쪽에 있는 테이블의 모든 결과를 가져 온 후 오른쪽/왼쪽 테이블의 데이터를 매칭하고, 매칭되는 데이터가 없는 경우 NULL로 표시한다.
-
ex) SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
List<Member> leftOuterJoin = em.createQuery("**select m from Member m left join m.team t**", Member.class) .getResultList(); // 결과 /* select m from Member m left join m.team t */ select member0_.id as id1_0_, member0_.age as age2_0_, member0_.TEAM_ID as team_id4_0_, member0_.username as username3_0_ from Member member0_ **left outer join** Team team1_ on member0_.TEAM_ID=team1_.id
-
-
카르테시안 곱(Cartesian product)을 통해서 모든 조합의 데이터를 만든 후 조인에 참여하는 두 릴레이션의 속성 값을 비교( =, <>, ≤, <, ≥, > )하여 조건을 만족하는 투플만 반환한다.
-
ex) SELECT count(m) FROM Member m, Team t WHERE m.username = t.name
List<Member> results = em.createQuery("**select m from Member m, Team t where m.username = t.name**", Member.class) .getResultList(); // 결과 /* select m from Member m, Team t where m.username = t.name */ select member0_.id as id1_0_, member0_.age as age2_0_, member0_.TEAM_ID as team_id4_0_, member0_.username as username3_0_ from Member member0_ **cross join** Team team1_ where member0_.username=team1_.name
-
연관 관계가 없는 데이터를 비교해보고 싶을 때 사용할 수 있다.
-
동등 조인- 세타 조인에서
=을 사용한 조인
- 세타 조인에서
-
자연 조인- 동등 조인에서 중복된 속성을 제거한 조인
-
세미 조인- 자연 조인 후 한쪽 릴레이션의 속성만 나타내는 조인
-
- JPA 2.1부터 지원하는 기능
-
조인 대상 필터링
-
조인을 하기 전 조인할 대상을 미리 필터링할 수 있다.
-
ON 절로 조회를 하게 되면 조인을 하기 전 ON 절에서 먼저 필터링하게 된다.
- ON 절이 아닌 WHERE 절로 할 경우에는 조인 후 WHERE 절 필터링을 하므로 다른 결과를 가져올 수 있다!
-
ex) 회원(Member)과 팀(Team)을 조인하면서, 팀 이름이 A인 팀만 조인
**JPQL** SELECT m, t FROM Member m LEFT JOIN m.team t **ON t.name = 'A'** SQL SELECT m.*, t.* FROM Member m LEFT JOIN Team t **ON m.TEAM_ID = t.id and t.name = 'A'**
-
-
연관 관계없는 엔티티를 외부 조인(Hibernate 5.1부터)
-
ex) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
**JPQL** SELECT m, t FROM Member m LEFT JOIN m.team t **ON m.username = t.name** SQL SELECT m.*, t.* FROM Member m LEFT JOIN Team t **ON m.username = t.name**
-
하나의 SQL 문에 포함되어 있는 또 다른 SQL 문을 의미한다.
-
JPQL도 서브 쿼리를 지원한다.
-
일반적인 서브 쿼리 작성과 똑같다.
-
ex) 나이가 평균보다 많은 회원
List<Member> results = em.createQuery("select m from Member m where m.age > **(select avg(m2.age) from Member m2)**", Member.class) .getResultList();
-
ex) 한 건이라도 주문한 고객
List<Member> results = em.createQuery("select m from Member m where **(select count(o) from Order o where m = o.member) > 0**", Member.class) .getResultList();
-
-
서브쿼리 사용시 주의사항
- 서브쿼리를 괄호로 감싸서 사용한다.
- 서브쿼리는 단일 행 또는 복수 행 비교 연산자와 함께 사용 가능하다.
- 서브쿼리에서는 ORDER BY 를 사용하지 못한다.
-
일반 서브쿼리가 사용 가능한 곳
- SELECT 절
- FROM 절
- WHERE 절
- HAVING 절
- ORDER BY 절
- INSERT 문의 VALUES 절
- UPDATE 문의 SET 절
-
[NOT] EXISTS (서브 쿼리) : 서브 쿼리에 결과가 하나라도 존재하면 참이 되는 함수
-
ex) teamA 소속인 회원
List<Member> results = em.createQuery("select m from Member m where **exists** (select t from m.team t where t.ame = 'teamA')", Member.class) .getResultList();
-
-
ALL | ANY | SOME (서브 쿼리)
-
ALL은 결과가 모두 만족했을 경우 참이다.
-
ex) 전체 상품 각각의 재고보다 주문량이 많은 주문들
List<Member> results = em.createQuery("select o from Order o where o.orderAmount > **ALL** (select p.stockAmount from Product p)", Member.class) .getResultList();
-
-
ANY, SOME은 하나라도 만족하면 참이다
-
ex) 어떤 팀이든 팀에 소속된 회원
List<Member> results = em.createQuery("select m from Member m where m.team = ANY (select t from Team t)", Member.class) .getResultList();
-
-
-
[NOT] IN (서브 쿼리) : 결과 중 하나라도 같은 값이 있으면 참
- ANY에서 비교 연산자
=사용한 것과 같다고 보면 된다. - ex) 어떤 팀이든 팀에 소속된 회원
List<Member> results = em.createQuery("select m from Member m where m.team in (select t from Team t)", Member.class) .getResultList();
- ANY에서 비교 연산자
- JPA 자체로는 WHERE, HAVING절에서만 서브 쿼리가 사용이 가능하다.
- BUT, 하이버네이트가 SELECT절에서 서브 쿼리를 사용할 수 있도록 지원해주어 WHERE, HAVING, SELECT에서 서브 쿼리 사용 가능하다고 보면 된다.
- 하지만 FROM 절의 서브 쿼리는 현재 JPQL에서 불가능하다.
- FROM 절 서브 쿼리 문제는 1. JOIN 절 사용 2. 쿼리 두 번 날리고 조합 3. 네이티브 SQL 사용 등의 방식으로 해결할 수 있다.
-
문자: ‘HELLO’, ‘She’’s’ -
숫자: 10L(Long), 10D(Double), 10F(Float) -
Boolean: TRUE, FALSE -
ENUM: package.MemberType.USER (패키지명 포함) 하드 코딩 또는 setParameter() 사용// ENUM 예시 1 String query = "SELECT m.username, 'HELLO', true FROM Member m where m.type = jpql.MemberType.USER"; // ENUM 예시 2 String query2 = "SELECT m.username, 'HELLO', true FROM Member m where m.type = :userType"; em.createQuery(query) .setParameter("userType", MemberType.USER)
-
엔티티 타입: TYPE(i) = Book (상속 관계에서 사용)em.createQuery("SELECT i FROM Item i where type(i) = Book", Item.class)
- SQL과 문법이 같은 식 모두 지원
- EXISTS, IN
- AND, OR, NOT
- =, >, >=, <, <=, <>
- BETWEEN, LIKE, IS NULL
-
기본 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
-
- JPQL의 기본 함수들로 DB에 상관없이 사용할 수 있다.
-
CONCAT
-
SUBSTRING
-
TRIM
-
LOWER, UPPER
-
LENGTH
-
LOCATE
-
ABS, SQRT, MOD
-
SIZE, INDEX(JPA 용도)
select size(t.members) from Team t; // Team의 List<Member> 컬렉션의 크기를 반환해준다. select index(t.members) from Team t; // @OrderColumn이 선언된 Team의 List<Member> 컬렉션의 값의 위치를 반환해준다. // 값이 삭제되고 생성되는 등 위치 순서가 바뀔 경우도 있기 때문에 사용 안하는 것이 좋다.
-
- JPQL에서 기본적으로 제공하지 않는 함수들은 Hibernate 방언에 추가해주어야 사용할 수 있다.
- 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록해야한다.
- 하지만,
org.hibernate.dialect에서 제공하는 DB 방언들을 보면 DB 종속적으로 사용할 수 있는 함수들이 추가 등록되어 있어 거의 해줄 필요가 없다. - 등록을 해야한다면,
org.hibernate.dialect에서 자신이 사용하는 DB 방언을 찾아 등록하는 방식을 참고하여 자신만의 DB 방언을 생성해준 뒤, DB 설정 property에서hibernate.dialect를 자신이 만든 DB방언으로 설정 후select custom_concat(i.name) from Item i이와 같이 사용해주면 된다.
- 하지만,
- 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록해야한다.
-
.을 찍어 객체 그래프를 탐색하는 것select m.username -> 상태 필드 from Member m join m.team t -> 단일 값 연관 필드 join t.members tm -> 컬렉션 값 연관 필드 where t.name = '팀A'
- 단순히 값을 저장하기 위한 필드
- ex) m.username
- 경로 탐색의 끝, 탐색 X
- 즉, m.username에서
.을 더 찍어 탐색을 할 수가 없다.
- 즉, m.username에서
- 연관 관계를 위한 필드
-
단일 값 연관 필드
-
@ManyToOne, @OneToOne
-
대상이 엔티티
- ex) m.team
List<Team> results = em.createQuery("select m.team From Member m", Team.class) .getResultList(); /* select m.team From Member m */ select team1_.id as id1_3_, team1_.name as name2_3_ from Member member0_ **inner join** Team team1_ on member0_.TEAM_ID=team1_.id
-
묵시적 내부 조인(INNER JOIN) 발생하며, 더 탐색을 할 수 있다.
- m.team**.name**, m.team.members 같이 더 탐색을 할 수 있다.
-
이렇게
묵시적 내부 조인이 발생하게 JPQL을 작성하면 안된다.조인을 발생하게 하더라도 외부에 명시할 수 있게 **JPQL을 SQL 쿼리처럼 작성(명시적 조인)**하는 것이 좋다. 그렇지 않으면 추후 성능 개선 및 쿼리 튜닝시 수 많은 쿼리에서 어떠한 부분을 개선해야할 지 찾기가 어려워진다.
-
-
컬렉션 값 연관 필드
-
@OneToMany, @ManyToMany
-
대상이 컬렉션
- ex) t.members
-
묵시적 내부 조인(INNER JOIN) 발생하며, 더 탐색을 할 수 없다.
Collection results = em.createQuery("select t.members From Team t", Collection.class) .getResultList();
- 컬렉션의 값을 여러 개이기 때문에,
.을 추가해 특정 값을 불러와 해당 값의 필드들을 가져올 방법이 없다. (size만 가져올 수 있다.)-
특정 값의 필드를 가져오려고 한다면, FROM 절에서 명시적 조인을 통해 별칭을 얻으면, 별칭을 통해 탐색이 가능하다.
List<String> results = em.createQuery("select m.username From Team t join t.members m", String.class) .getResultList();
-
- 컬렉션의 값을 여러 개이기 때문에,
- Join 키워드 직접 사용
- 경로 표현식에 의해 묵시적으로 SQL 조인 발생(내부 조인만 가능)
묵시적 조인은 사용하지 말아야 한다.
- 항상 내부 조인이 일어난다.
- 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야 한다.
- 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 준다.
-
SQL 조인 종류X
-
JPQL에서 성능 최적화를 위해 제공하는 기능
-
연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
-
join fetch 명령어 사용
Fetch join ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
-
-
Join 사용 x, 일반 Join 사용, Fetch Join 사용 간단한 예시 및 결과
-
모든 회원 조회 (join x)
// join X String query = "SELECT m FROM Member m"; List<Member> results = em.createQuery(query, Member.class) .getResultList(); for (Member member : results) { System.out.println("member = " + member.getUsername()); } Hibernate: /* SELECT m FROM Member m */ select member0_.id as id1_0_, member0_.age as age2_0_, member0_.TEAM_ID as team_id4_0_, member0_.username as username3_0_ from Member member0_ member = 회원1 member = 회원2 member = 회원3
-
회원들이 속해있는 팀의 회원만 조회 (일반 join)
- Fetch join과 차이점은 연관된 엔티티를 함께 조회하지 않는다. 그러므로 team에 대한 데이터를 불러올 시 추가 쿼리를 전송한다.
즉, 결과 반환 시 연관관계를 고려하지 않고 SELECT 절에 지정한 엔티티만 조회한다.
// 일반 inner join String query = "SELECT m FROM Member m join m.team"; List<Member> results = em.createQuery(query, Member.class) .getResultList(); for (Member member : results) { System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName()); } Hibernate: /* SELECT m FROM Member m join m.team */ select member0_.id as id1_0_, member0_.age as age2_0_, member0_.TEAM_ID as team_id4_0_, member0_.username as username3_0_ from Member member0_ inner join Team team1_ on member0_.TEAM_ID=team1_.id Hibernate: select team0_.id as id1_3_0_, team0_.name as name2_3_0_ from Team team0_ where team0_.id=? member = 회원1, 팀A member = 회원2, 팀A Hibernate: select team0_.id as id1_3_0_, team0_.name as name2_3_0_ from Team team0_ where team0_.id=? member = 회원3, 팀B
-
회원을 조회하면서 연관된 팀도 함께 조회 (Fetch join - SQL 한 번에)
// inner join fetch String query = "SELECT m FROM Member m join fetch m.team"; List<Member> results = em.createQuery(query, Member.class) .getResultList(); for (Member member : results) { System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName()); } Hibernate: /* SELECT m FROM Member m join fetch m.team */ select member0_.id as id1_0_0_, team1_.id as id1_3_1_, member0_.age as age2_0_0_, member0_.TEAM_ID as team_id4_0_0_, member0_.username as username3_0_0_, team1_.name as name2_3_1_ from Member member0_ inner join Team team1_ on member0_.TEAM_ID=team1_.id member = 회원1, 팀A member = 회원2, 팀A member = 회원3, 팀B
[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,팀B,회원1(팀A),회원2(팀A),회원3(팀B)가 DB에 저장되어 있다는 가정 -
어떠한 Join도 사용하지 않았을 경우
List<Member> results = em.createQuery("SELECT m FROM Member m", Member.class) .getResultList(); for (Member member : results) { System.out.println("member = " + member.getUsername() + "," + member.getTeam().getName()); }
Hibernate: // 전체 회원에 대한 쿼리 /* SELECT m FROM Member m */ select member0_.id as id1_0_, member0_.age as age2_0_, member0_.TEAM_ID as team_id4_0_, member0_.username as username3_0_ from Member member0_ Hibernate: // 팀A에 대한 쿼리 select team0_.id as id1_3_0_, team0_.name as name2_3_0_ from Team team0_ where team0_.id=? member = 회원1, 팀A member = 회원2, 팀A Hibernate: // 팀B에 대한 쿼리 select team0_.id as id1_3_0_, team0_.name as name2_3_0_ from Team team0_ where team0_.id=? member = 회원3, 팀B
-
Member의 Team 필드는 현재
@ManyToOne(fetch = FetchType.LAZY)로 설정되어 있기 때문에프록시로 생성된다. -
member.getTeam() 호출 할 시 해당 팀이 영속성 컨텍스트에 존재하지 않는다면, 쿼리를 생성하여 DB 조회를 한 후 얻어온 값을 영속성 컨텍스트에 저장하고, 해당 값을 통해 실제 엔티티를 생성하고 실제 엔티티의 getTeam()으로부터 값을 받아오게 된다. (영속성 컨텍스트에 존재한다면, 영속성 컨텍스트(1차 캐시)에서 바로 값을 받아온다.)
-
회원1에 대한팀A에 대한 정보가 없기 때문에 쿼리를 통한 DB 조회 후 영속성 컨텍스트에 저장 - SQL -
회원2는회원1을 통해 영속성 컨텍스트에 저장되어 있는팀A를 가져오기 때문에 DB 조회 X - 영속성 1차 캐시 -
회원3은팀B에 대한 정보가 없기 때문에 쿼리를 통한 DB 조회 후 영속성 컨텍스트에 저장 - SQL→ 회원 조회 SQL + 팀A 조회 SQL + 팀B 조회 SQL = 총 3번의 SQL
-
-
만약, 회원 100명이며 모두 서로 다른 팀 소속일 경우 쿼리가 101번(회원 SQL 1 + 팀 SQL 100) DB에 전송된다.
- 이러한 문제를 **
N + 1 문제**라고 한다.-
1은 회원을 가져오기 위한 쿼리 1개 생성을 의미 -
N은1에서 얻어온 결과의 크기 N만큼의 쿼리가 더 생성된다는 의미 -
즉시 로딩,지연 로딩모두에서 발생할 수 있다.- 이러한 문제를 해결하기 위해서
Fetch join을 사용한다.
- 이러한 문제를 해결하기 위해서
-
- 이러한 문제를 **
-
Fetch join 사용
String query = "SELECT m FROM Member m **join fetch** m.team"; List<Member> results = em.createQuery(query, Member.class) .getResultList(); for (Member member : results) { System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName()); } Hibernate: /* SELECT m FROM Member m join fetch m.team */ select member0_.id as id1_0_0_, **team1_.id as id1_3_1_,** member0_.age as age2_0_0_, member0_.TEAM_ID as team_id4_0_0_, member0_.username as username3_0_0_, **team1_.name as name2_3_1_** from Member member0_ inner join Team team1_ on member0_.TEAM_ID=team1_.id member = 회원1, 팀A member = 회원2, 팀A member = 회원3, 팀B
- 이전과는 다르게 Member의 Team 필드가 프록시가 아닌
**실제 Entity로 존재**하게 되고, 모든 값들이 영속성 컨텍스트에 저장되어 있기 때문에 값을 불러올 때 **영속성 1차 캐시**에서 가져와 **총 쿼리가 SELECT 1번**만 전송이 된다는 것을 볼 수 있다.
- 이전과는 다르게 Member의 Team 필드가 프록시가 아닌
-
팀A,팀B,회원1(팀A),회원2(팀A),회원3(팀B)가 DB에 저장되어 있다는 가정 -
Collection Fetch join
[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'
String query = "SELECT t FROM Team t join fetch t.members"; List<Team> results = em.createQuery(query, Team.class) .getResultList(); for (Team team : results) { System.out.println("team = " + team.getName() + " & members = " + team.getMembers().size()); for (Member member : team.getMembers()) { System.out.println("-> member = " + member); } } Hibernate: /* SELECT t FROM Team t join fetch t.members */ select team0_.id as id1_3_0_, members1_.id as id1_0_1_, team0_.name as name2_3_0_, members1_.age as age2_0_1_, members1_.TEAM_ID as team_id4_0_1_, members1_.username as username3_0_1_, members1_.TEAM_ID as team_id4_0_0__, members1_.id as id1_0_0__ from Team team0_ inner join Member members1_ on team0_.id=members1_.TEAM_ID team = 팀A & members = 2 -> member = Member{id=3, username='회원1', age=0} -> member = Member{id=4, username='회원2', age=0} team = 팀A & members = 2 -> member = Member{id=3, username='회원1', age=0} -> member = Member{id=4, username='회원2', age=0} team = 팀B & members = 1 -> member = Member{id=5, username='회원3', age=0}
-
fetch join을 사용했기 때문에 쿼리는 한 번만 생성이 되고, 결과물인
팀A의 회원 2명,팀B의 회원 1명도 정확하다. 하지만,팀A에 대한중복 결과가 출력이 된다. 바로 이 점이 Collection fetch join시 주의해야할 사항이다.- 팀에 소속된 각 회원과 조합(Cartesian product)이 되어 팀 테이블은 결국
팀A : 회원1,팀A : 회원2,팀B : 회원3이렇게 3개의 값을 가지게 되고, for문을 통해team.getMembers().size()를 할 경우, 3개의 값에 대한 회원들의 수를 출력하게 될텐데팀A 2개,팀B 1개이기 때문에팀A에 대한getMembers().size()가 2번 출력되는 문제가 발생하게 된다. 즉, 중복된 데이터로 인해 총 데이터의 크기가 커질 수 있다. - SQL의 DISTINCT 기능을 통해 중복된 결과를 제거할 수 있지 않을까 생각할 수 있지만, 그렇지 않다.
- DISTINCT는 모든 속성의 값이 같아야지만 제거해주는 기능인데
팀A : 회원 1,팀A : 회원 2는 회원의 값이 다르기 때문에 데이터를 제거하지 못한다. - 그러면, 어떻게
팀A중복 제거를 할 수 있을까?..
- DISTINCT는 모든 속성의 값이 같아야지만 제거해주는 기능인데
- 팀에 소속된 각 회원과 조합(Cartesian product)이 되어 팀 테이블은 결국
-
해결방법 ⇒ JPQL의 DISTINCT 사용
-
JPQL DISTINCT가 제공하는 기능
- SQL에 DISTINCT 추가
- 애플리케이션에서 엔티티 중복 제거
같은 식별자를 가진 엔티티를 제거한다.
String query = "SELECT DISTINCT t FROM Team t join fetch t.members"; List<Team> results = em.createQuery(query, Team.class) .getResultList(); for (Team team : results) { System.out.println("team = " + team.getName() + " & members = " + team.getMembers().size()); for (Member member : team.getMembers()) { System.out.println("-> member = " + member); } } Hibernate: /* SELECT DISTINCT t FROM Team t join fetch t.members */ select distinct team0_.id as id1_3_0_, members1_.id as id1_0_1_, team0_.name as name2_3_0_, members1_.age as age2_0_1_, members1_.TEAM_ID as team_id4_0_1_, members1_.username as username3_0_1_, members1_.TEAM_ID as team_id4_0_0__, members1_.id as id1_0_0__ from Team team0_ inner join Member members1_ on team0_.id=members1_.TEAM_ID team = 팀A & members = 2 -> member = Member{id=3, username='회원1', age=0} -> member = Member{id=4, username='회원2', age=0} team = 팀B & members = 1 -> member = Member{id=5, username='회원3', age=0}
- JPA에서 팀의 식별자를 확인한 후,
팀A중복된 식별자에 대한 데이터를 제거해주어팀A,팀B총 2개의 값만 가져오게 된다.
-
- Hibernate는 가능하지만, 가급적 사용하지 않는 것이 좋다.
- Fetch join은 연관 엔티티를 모두 가져와서 사용하기 위한 기능인데 별칭을 사용하여 where 조건절 등에 사용하여 일부 엔티티만 걸러서 가져올 경우, Fetch join의 목적성과는 다르게 된다. 그러므로 일부만 가져올 경우 별도의 쿼리를 사용해라.
- 아까 위의 OneToMany Fetch join에서 같은 식별자를 가지지만 다른 속성 값을 가지는 데이터의 추가로 총 데이터의 크기가 커지는 것처럼 굉장히 데이터가 커질 수 있는 위험성이 있기 때문에 막혀있다.
- 간혹, 되는 경우가 있다고 하지만 데이터가 불일치 하는 경우가 굉장히 많다고 한다.
-
OneToMany Fetch join에서 같은 식별자를 가지지만 다른 속성 값을 가지는 데이터의 추가로 총 데이터의 크기가 커지는데, 이 때 페이지 1개만 가져오려고 할 경우, 하나의 식별자는 여러 개의 값을 가지고 있지만, 페이지 1개를 가져오게 되면 해당 식별자에 속하는 데이터는 1개밖에 없다는 결과로 비춰질 수 있기 때문에 사용할 수 없게 막아놓았다.
-
OneToOne, ManyToOne 같은 단일 값 연관 필드들은 Fetch join해도 페이징 가능
-
Hibernate는 경고 로그를 남기고 메모리에서 페이징한다(매우 위험하다.)
String query = "SELECT t FROM Team t join fetch t.members m"; List<Team> results = em.createQuery(query, Team.class) .setFirstResult(0) .setMaxResults(1) .getResultList(); for (Team team : results) { System.out.println("team = " + team.getName() + " & members = " + team.getMembers().size()); for (Member member : team.getMembers()) { System.out.println("-> member = " + member); } } **WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!** Hibernate: /* SELECT t FROM Team t join fetch t.members m */ select team0_.id as id1_3_0_, members1_.id as id1_0_1_, team0_.name as name2_3_0_, members1_.age as age2_0_1_, members1_.TEAM_ID as team_id4_0_1_, members1_.username as username3_0_1_, members1_.TEAM_ID as team_id4_0_0__, members1_.id as id1_0_0__ from Team team0_ inner join Member members1_ on team0_.id=members1_.TEAM_ID team = 팀A & members = 2 -> member = Member{id=3, username='회원1', age=0} -> member = Member{id=4, username='회원2', age=0}
- 객체 그래프의 특성상 위에서 Team을 불러올 경우 필드 members에 대한 모든 값을 포함하여 불러올 수 있게 해야한다. 그래서 하나만 페이징을 하려고 하더라도 member의 수가 100만개가 있다고 할 경우, 100만개 모두 메모리에 올라간 후 페이징 하게 되어 굉장히 위험한 상황에 놓일 수 있게 된다.
해결 방안 1. SQL을 ManyToOne 방향으로 변경하여 작성
// 기존 "SELECT t FROM Team t join fetch t.members m"; // 변경 "SELECT m FROM Member m join fetch m.team t";
해결 방안 2. Fetch join 제거 및 setMaxResults()를 존재하는 엔티티 갯수 만큼 설정한 뒤, 값을 가져올 때마다 쿼리를 생성하는 Lazy-Loading 방식 사용
String query = "SELECT t FROM Team t"; List<Team> results = em.createQuery(query, Team.class) .setFirstResult(0) .setMaxResults(2) .getResultList(); for (Team team : results) { System.out.println("team = " + team.getName() + " & members = " + team.getMembers().size()); for (Member member : team.getMembers()) { System.out.println("-> member = " + member); } } Hibernate: /* SELECT t FROM Team t */ select team0_.id as id1_3_, team0_.name as name2_3_ from Team team0_ limit ? Hibernate: select members0_.TEAM_ID as team_id4_0_0_, members0_.id as id1_0_0_, members0_.id as id1_0_1_, members0_.age as age2_0_1_, members0_.TEAM_ID as team_id4_0_1_, members0_.username as username3_0_1_ from Member members0_ where members0_.TEAM_ID=? team = 팀A & members = 2 -> member = Member{id=3, username='회원1', age=0} -> member = Member{id=4, username='회원2', age=0} Hibernate: select members0_.TEAM_ID as team_id4_0_0_, members0_.id as id1_0_0_, members0_.id as id1_0_1_, members0_.age as age2_0_1_, members0_.TEAM_ID as team_id4_0_1_, members0_.username as username3_0_1_ from Member members0_ where members0_.TEAM_ID=? team = 팀B & members = 1 -> member = Member{id=5, username='회원3', age=0}
- 하나씩 조회 할때마다 쿼리가 생성되기 때문에 성능상 굉장히 좋지 않은 방식이다.
- 그리고 이러한 문제를 이전에 정의했던 **
N + 1 문제**라고 볼 수 있다.-
1: 모든 Team 조회 쿼리 1개 -
N: 각 Team에 대한 lazy-loading이 적용되어 있는 members 조회 쿼리 N개 - 이전에
N + 1 문제를Fetch join을 통해 해결할 수 있다고 했지만 Collection에서 페이징 처리할 경우에는 Fetch join을 사용할 수 없기 때문에 이 문제를 해결하기 위해서는@BatchSize를 사용해야 한다.
-
해결 방안 3. @BatchSize() 사용
@Entity public class Team { @Id @GeneratedValue private Long id; private String name; **@BatchSize(size = 100)** @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>();String query = "SELECT t FROM Team t"; List<Team> results = em.createQuery(query, Team.class) .setFirstResult(0) .setMaxResults(2) .getResultList(); for (Team team : results) { System.out.println("team = " + team.getName() + " & members = " + team.getMembers().size()); for (Member member : team.getMembers()) { System.out.println("-> member = " + member); } } Hibernate: /* SELECT t FROM Team t */ select team0_.id as id1_3_, team0_.name as name2_3_ from Team team0_ limit ? Hibernate: /* load one-to-many jpql.Team.members */ select members0_.TEAM_ID as team_id4_0_1_, members0_.id as id1_0_1_, members0_.id as id1_0_0_, members0_.age as age2_0_0_, members0_.TEAM_ID as team_id4_0_0_, members0_.username as username3_0_0_ from Member members0_ where members0_.TEAM_ID in ( **?, ? // 팀A ID, 팀B ID** ) team = 팀A & members = 2 -> member = Member{id=3, username='회원1', age=0} -> member = Member{id=4, username='회원2', age=0} team = 팀B & members = 1 -> member = Member{id=5, username='회원3', age=0}
-
"SELECT t FROM Team t"로 Team을 불러올 때,@BatchSize에 선언한 개수만큼in (?, ?)에 존재하는 Team ID들을 넘겨준다. 100으로 설정되어 한 번에 100개의 Team ID를 넘겨줄 수 있지만, 현재팀A, 팀B2개밖에 존재하지 않기 때문에 2개만 넘겨준다. 만약 Team이 150개가 존재한다면, 100개, 50개 2번 전송하는 방식으로 동작한다.- 이를 통해, 쿼리가
N+1이 아닌테이블의 수만큼만 생성이 된다.
- 이를 통해, 쿼리가
- 위에 코드에서는 Team Entity 내에 @BatchSize()를 선언해줬지만, 일반적으로 보통 전역적으로 사용하기 때문에 property, yml 같은 설정 파일에서
hibernate.default_batch_fetch_size= 1000 이하의 숫자를 넣어주고 사용한다.
-
연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
-
엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선순위가 높다.
- ex) @OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
-
실무에서 글로벌 로딩 전략은 모두 지연 로딩
-
최적화가 필요한 곳은 Fetch join 적용
- ex) N + 1
-
Fetch join은 객체 그래프를 유지할 때 사용하면 효과적이다.
- ex)
.을 사용하여 탐색할 때m.team
- ex)
-
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, Fetch join 보다는 일반 join을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
-
조회 대상을 특정 자식으로 한정
-
ex) 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’)
-
자바의 타입 캐스팅과 유사
-
상속 구조에서 부모타입을 특정 자식타입으로 다룰때 사용
-
FROM, WHERE, SELECT(Hibernate 지원) 사용
-
ex) 부모인 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
-
엔티티를 파라미터로 전달
String jpql = “select m from Member m where **m** = **:member**”; List resultList = em.createQuery(jpql) .setParameter("member", **member**) .getResultList();
-
식별자를 직접 전달
String jpql = “select m from Member m where **m.id = :memberId**”; List resultList = em.createQuery(jpql) .setParameter("memberId", **memberId**) .getResultList();
-
실행되는 SQL
select m.* from Member m where **m.id=?**
-
엔티티를 파라미터로 전달
Team **team** = em.find(Team.class, 1L); String jpql = “select m from Member m where **m.team** = **:team**”; List resultList = em.createQuery(jpql) .setParameter("**team**", **team**) .getResultList();
-
식별자를 직접 전달
String jpql = “select m from Member m where **m.team.id = :teamId**”; List resultList = em.createQuery(jpql) .setParameter("**teamId**", **teamId**) .getResultList();
-
실행되는 SQL
select m.* from Member m where **m.team_id=?**
-
미리 정의해서 이름을 부여해두고 사용하는 JPQL
-
정적 쿼리
-
어노테이션, XML에 정의
-
XML이 항상 우선권을 가진다.
-
애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.
-
어노테이션
@Entity @NamedQuery(name = "Member.findByUsername", query="select m from Member m where m.username = :username") public class Member { ... } List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class) .setParameter("username", "회원1") .getResultList();
-
XML
**[META-INF/persistence.xml]** <persistence-unit name="jpabook" ><mapping-file>META-INF/ormMember.xml</mapping-file> **[META-INF/ormMember.xml]** <?xml version="1.0" encoding="UTF-8"?> <entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1"> <named-query name="Member.findByUsername"> <query><![CDATA[ select m from Member m where m.username = :username ]]></query> </named-query> <named-query name="Member.count"> <query>select count(m) from Member m</query> </named-query> </entity-mappings>
-
-
애플리케이션 로딩 시점에 초기화 후 재사용
-
애플리케이션 로딩 시점에 쿼리를 검증
- 쿼리 문법 등 대부분의 오류를 감지한다.
-
Named Query보다는 Spring Data JPA의 이름 없는 Named Query를 사용하는 방법을 더 선호한다.
public interface MemberRepository extends JpaRepository<Member, Long> { @Query("select m from Member m where m.username = :username") List<User> findByUsername(String username); }
-
쿼리 한 번으로 여러 테이블 로우 변경(엔티티)하는 연산
-
재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
-
JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL실행
- 재고가 10개 미만인 상품을 리스트로 조회한다.
- 상품 엔티티의 가격을 10% 증가한다.
- 트랜잭션 커밋 시점에 변경감지가 동작한다.
- 변경 된 데이터가 100건이라면 100번의 UPDATE SQL 실행
-
JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL실행
-
UPDATE, DELETE 지원
- INSERT(insert into .. select, Hibernate 지원)
-
ex) 모든 회원의 나이를 20살로 수정
int resultCount = em.createQuery("update Member m set m.age = 20") .executeUpdate(); System.out.println("resultCount = " + resultCount);
-
executeUpdate()는 영향받은 엔티티의 수를 반환한다.
-
- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 전송한다.
- 사용 방법
- 벌크 연산을 먼저 실행
-
벌크 연산 수행 후 영속성 컨텍스트 초기화
- 벌크 연산을 통해
A의 데이터를 수정하였는데, 벌크 연산 이전에 수행했던 로직의 결과로 과거의A엔티티가 영속성 컨텍스트에 남아있을 경우, 서로 다른A가 존재하기 때문에 영속성 컨텍스트를 초기화해주는 것이 좋다.
- 벌크 연산을 통해
- Spring Data JPA의 @Modifying, @Query(쿼리)를 활용한 벌크 연산을 해라.