Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Seongbeen Kim edited this page Jun 8, 2021 · 2 revisions

JPQL 문법

  • 엔티티와 속성은 대소문자 구분한다.

    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, Query

  • 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); 으로 해줄 수 있다.

결과 조회 API

getResultList()

  • 결과가 하나 이상일 때, 결과가 담긴 리스트 반환
  • 결과가 없으면, 빈 리스트 반환
    • NullPointerException을 걱정할 필요가 없다.

getSingleResult()

  • 결과가 정확히 하나일 경우에만, 단일 객체 반환
  • 결과가 없으면: 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를 추가하여 중복 제거도 가능하다.

엔티티 프로젝션 (Member entity)

  • 아래의 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에 반영이 된다.

      update
      • 즉, 엔티티 프로젝션 em.createQuery("select m from Member m", Member.class) 을 통해 가져오는 모든 Member 는 영속성 컨텍스트에 의해 관리가 된다고 보면 된다.

엔티티 프로젝션 (Team entity)

  • 아래의 코드는 어떤 쿼리를 날리게 될까?

    .
    List<Member> result = em.createQuery("select m.team from Member m", Team.class)
    												.getResultList();
    inner join
    • 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();
    embedded
    • Address 관련된(city, street, zipcode) 정보를 조회하는 쿼리를 잘 생성하는 것을 볼 수 있다.
      • Address가 @Embeddable로 선언된 값 객체이기 때문에 select a from Address a 처럼 쿼리를 만들수는 없다. 즉, 임베디드 타입 프로젝션은 select o.address from Order o 처럼 엔티티로부터 시작해야 한다.

스칼라 타입 프로젝션

  • 아래의 코드는 어떤 쿼리를 날리게 될까?

    em.createQuery("select distinct m.username, m.age from Member m", Member.class)
    	.getResultList();
    scala
    • 스칼라 타입 프로젝션은 일반 SQL의 프로젝션과 똑같다고 보면 된다.

여러 값 조회

  1. 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
  2. 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
  3. 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

페이징 API

setFirstResult(int startPosition())

  • 조회 시작 위치

setMaxResult(int maxResult())

  • 조회할 데이터 수
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 ?

조인

내부 조인([INNER] JOIN)

  • 둘 이상의 테이블에 존재하는 공통 속성의 값이 같은 것을 결과로 추출
    • 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

외부 조인([OUTER] JOIN)

  • 왼쪽/오른쪽에 있는 테이블의 모든 결과를 가져 온 후 오른쪽/왼쪽 테이블의 데이터를 매칭하고, 매칭되는 데이터가 없는 경우 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
    • 연관 관계가 없는 데이터를 비교해보고 싶을 때 사용할 수 있다.

    • 동등 조인

      • 세타 조인에서 = 을 사용한 조인
    • 자연 조인

      • 동등 조인에서 중복된 속성을 제거한 조인
    • 세미 조인

      • 자연 조인 후 한쪽 릴레이션의 속성만 나타내는 조인

ON 절

  • JPA 2.1부터 지원하는 기능
  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'**
  2. 연관 관계없는 엔티티를 외부 조인(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();
  • 서브쿼리 사용시 주의사항

    1. 서브쿼리를 괄호로 감싸서 사용한다.
    2. 서브쿼리는 단일 행 또는 복수 행 비교 연산자와 함께 사용 가능하다.
    3. 서브쿼리에서는 ORDER BY 를 사용하지 못한다.
  • 일반 서브쿼리가 사용 가능한 곳

    1. SELECT 절
    2. FROM 절
    3. WHERE 절
    4. HAVING 절
    5. ORDER BY 절
    6. INSERT 문의 VALUES 절
    7. 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();

JPA 서브 쿼리 한계

  • JPA 자체로는 WHERE, HAVING절에서만 서브 쿼리가 사용이 가능하다.
    • BUT, 하이버네이트가 SELECT절에서 서브 쿼리를 사용할 수 있도록 지원해주어 WHERE, HAVING, SELECT에서 서브 쿼리 사용 가능하다고 보면 된다.
  • 하지만 FROM 절의 서브 쿼리는 현재 JPQL에서 불가능하다.
    • FROM 절 서브 쿼리 문제는 1. JOIN 절 사용 2. 쿼리 두 번 날리고 조합 3. 네이티브 SQL 사용 등의 방식으로 해결할 수 있다.

JPQL 타입 표현

  • 문자: ‘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)

JPQL 기타

  • SQL과 문법이 같은 식 모두 지원
  • EXISTS, IN
  • AND, OR, NOT
  • =, >, >=, <, <=, <>
  • BETWEEN, LIKE, IS NULL

조건식

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
  • COALESCE

    • 조회 값이 null이면 오른쪽 값으로 대체하여 반환

      • ex) 사용자 이름이 없으면 '**이름 없는 회원'**을 반환
      select coalesce(m.username,'이름 없는 회원') from Member m
  • NULLIF

    • 두 값이 같으면 null 반환, 다르면 첫 번째 값 반환

      • ex) 사용자 이름이 **‘관리자’**면 null을 반환하고 나머지는 본인의 이름을 반환
      select nullif(m.username, '관리자') from Member m

JPQL 기본 함수

  • 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 이와 같이 사용해주면 된다.

경로 표현식

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

    select m.username -> 상태 필드 
    from Member m
    join m.team t -> 단일 값 연관 필드
    join t.members tm -> 컬렉션 값 연관 필드
    where t.name = '팀A'

상태 필드

  • 단순히 값을 저장하기 위한 필드
    • ex) m.username
  • 경로 탐색의 끝, 탐색 X
    • 즉, m.username에서 . 을 더 찍어 탐색을 할 수가 없다.

연관 필드

  • 연관 관계를 위한 필드
  1. 단일 값 연관 필드

    • @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 쿼리처럼 작성(명시적 조인)**하는 것이 좋다. 그렇지 않으면 추후 성능 개선 및 쿼리 튜닝시 수 많은 쿼리에서 어떠한 부분을 개선해야할 지 찾기가 어려워진다.

  2. 컬렉션 값 연관 필드

  • @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) 절에 영향을 준다.

Fetch 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

@ManyToOne Fetch join

  • 팀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개 생성을 의미
      • N1 에서 얻어온 결과의 크기 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번**만 전송이 된다는 것을 볼 수 있다.

@OneToMany Fetch join

  • 팀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 중복 제거를 할 수 있을까?..
    • 해결방법 ⇒ JPQL의 DISTINCT 사용

    • JPQL DISTINCT가 제공하는 기능

      1. SQL에 DISTINCT 추가
      2. 애플리케이션에서 엔티티 중복 제거
        • 같은 식별자를 가진 엔티티를 제거한다.
      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개의 값만 가져오게 된다.

Fetch join의 특징과 한계

Fetch join 대상에는 별칭을 줄 수 없다.

  • Hibernate는 가능하지만, 가급적 사용하지 않는 것이 좋다.
  • Fetch join은 연관 엔티티를 모두 가져와서 사용하기 위한 기능인데 별칭을 사용하여 where 조건절 등에 사용하여 일부 엔티티만 걸러서 가져올 경우, Fetch join의 목적성과는 다르게 된다. 그러므로 일부만 가져올 경우 별도의 쿼리를 사용해라.

둘 이상의 Collection은 Fetch join할 수 없다.

  • 아까 위의 OneToMany Fetch join에서 같은 식별자를 가지지만 다른 속성 값을 가지는 데이터의 추가로 총 데이터의 크기가 커지는 것처럼 굉장히 데이터가 커질 수 있는 위험성이 있기 때문에 막혀있다.
    • 간혹, 되는 경우가 있다고 하지만 데이터가 불일치 하는 경우가 굉장히 많다고 한다.

Collection을 Fetch join하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.

  • 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, 팀B 2개밖에 존재하지 않기 때문에 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
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, Fetch join 보다는 일반 join을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.

다형성 쿼리

TYPE

  • 조회 대상을 특정 자식으로 한정

  • 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’)

TREAT(JPA 2.1)

  • 자바의 타입 캐스팅과 유사

  • 상속 구조에서 부모타입을 특정 자식타입으로 다룰때 사용

  • 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=?**

Named 쿼리

정적 쿼리

  • 미리 정의해서 이름을 부여해두고 사용하는 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실행
      1. 재고가 10개 미만인 상품을 리스트로 조회한다.
      2. 상품 엔티티의 가격을 10% 증가한다.
      3. 트랜잭션 커밋 시점에 변경감지가 동작한다.
      • 변경 된 데이터가 100건이라면 100번의 UPDATE 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() 는 영향받은 엔티티의 수를 반환한다.

주의사항

  • 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 전송한다.
  • 사용 방법
    1. 벌크 연산을 먼저 실행
    2. 벌크 연산 수행 후 영속성 컨텍스트 초기화
      • 벌크 연산을 통해 A 의 데이터를 수정하였는데, 벌크 연산 이전에 수행했던 로직의 결과로 과거의 A 엔티티가 영속성 컨텍스트에 남아있을 경우, 서로 다른 A 가 존재하기 때문에 영속성 컨텍스트를 초기화해주는 것이 좋다.
    3. Spring Data JPA의 @Modifying, @Query(쿼리)를 활용한 벌크 연산을 해라.

Clone this wiki locally