[JPA] 11. 객체지향 쿼리 언어(JPQL) - 중급

최진민·2021년 6월 6일
0

JPA

목록 보기
11/11
post-thumbnail

경로 표현식 (객체 그래프)

  • 경로 표현식 : .을 찍어 객체 그래프를 탐색

    select m.username // 상태 필드
    	from Member m
    		join m.team t // 단일 값 연관 필드
    		join m.orders o // 컬렉션 값 연관 필드
    where t.name = '팀A'
  • 상태 필드 : 단순히 값을 저장하기 위한 필드

  • 연관 필드 : 연관관계를 위한 필드

    • 단일 값 연관 필드
      • @ManyToOne, @OneToOne 대상이 엔티티
      • @OneToMany, @ManyToMany 대상이 컬렉션
  • 💋특징

    • 1) 상태 필드 : 경로 탐색의 끝, 탐색 X
    • 2) 단일 값 연관 경로 : 묵시적 내부 조인 발생, 탐색 O
    • 3) 컬렉션 값 연관 경로 : 묵시적 내부 조인 발생, 탐색 X
      • BUT, from 절에서 명시적 조인을 통해 별칭을 얻으면 탐색 가능
    • 🧨실무 → 묵시적 조인 절대 지양! 명시적 조인 사용 권장!
  • 명시적, 묵시적 조인

    • 명시적 조인 : join키워드 사용
      • select t from Member m join m.team t
    • 묵시적 조인 : 경로 표현식에 의해 묵시적으로 sql 조인 발생(only 내부 조인)
      • select m.team from Member m

페치 조인 1 - 기본

  • fetch join

    • SQL 조인 X
    • JPQL 전용 성능 최적화
    • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회(즉시 로딩)
  • 1) 엔티티 페치 조인

    • 회원을 조회하면서 연관된 팀도 함께 조회하고 싶다

      • 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
    • 예제 살펴보기

      try {
      
              Team team1 = new Team();
              team1.setName("팀A");
              em.persist(team1);
      
              Team team2 = new Team();
              team2.setName("팀B");
              em.persist(team2);
      
              Team team3 = new Team();
              team3.setName("팀C");
              em.persist(team3);
      
              Member member1 = new Member();
              member1.setUsername("회원1");
              member1.setTeam(team1);
              em.persist(member1);
      
              Member member2 = new Member();
              member2.setUsername("회원2");
              member2.setTeam(team1);
              em.persist(member2);
      
              Member member3 = new Member();
              member3.setUsername("회원3");
              member3.setTeam(team2);
              em.persist(member3);
      
              Member member4 = new Member();
              member4.setUsername("회원4");
              em.persist(member4);
      
              em.flush(); //db에 반영
              em.clear(); //영속성 컨텍스트 비우기
      
              /**
               * query = "select m from Member m"
               * 쿼리 => N + 1(처음 멤버)
               * 팀A => SQL
               * 팀A => 1차캐시(영속성컨텍스트)
               * 팀B => SQL
               */
              String query = "select m from Member m join fetch m.team";
      
              List<Member> members = em.createQuery(query, Member.class)
                      .getResultList();
      
              for (Member member : members) {
                  System.out.println("회원 : " + member.getUsername() +
                          "\t" + "속한 팀 : " + member.getTeam().getName());
              }
              tx.commit();
          }
      print:
      회원 : 회원1	속한 팀 : 팀A
      회원 : 회원2	속한 팀 : 팀A
      회원 : 회원3	속한 팀 : 팀B
      //But, fetch join으로 sql은 한 번 생성
  • 2) 컬렉션 페치 조인

    • 일대다 관계, 데이터가 뻥튀기 될 수 있다.

    • JPQL : selelct 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'

    • 예제 살펴보기)

      try {
      
              Team team1 = new Team();
              team1.setName("팀A");
              em.persist(team1);
      
              Team team2 = new Team();
              team2.setName("팀B");
              em.persist(team2);
      
              Team team3 = new Team();
              team3.setName("팀C");
              em.persist(team3);
      
              Member member1 = new Member();
              member1.setUsername("회원1");
              member1.setTeam(team1);
              em.persist(member1);
      
              Member member2 = new Member();
              member2.setUsername("회원2");
              member2.setTeam(team1);
              em.persist(member2);
      
              Member member3 = new Member();
              member3.setUsername("회원3");
              member3.setTeam(team2);
              em.persist(member3);
      
              Member member4 = new Member();
              member4.setUsername("회원4");
              em.persist(member4);
      
              em.flush(); //db에 반영
              em.clear(); //영속성 컨텍스트 비우기
      
              String query = "select t from Team t join fetch t.members";
              
              List<Team> teams = em.createQuery(query, Team.class)
                      .getResultList();
      
              for (Team team : teams) {
                  System.out.println("<" + team.getName() + ">에 속한 회원수 : " + team.getMembers().size());
              }
      
              tx.commit();
          }
      print:
      <팀A>에 속한 회원수 : 2
      <팀A>에 속한 회원수 : 2
      <팀B>에 속한 회원수 : 1
      //fetch join으로 sql은 한 번 생성
      //중복 결과 이유 : 팀에 속한 Member가 2개
  • 페치 조인과 DISTINCT

    • JPQL의 2가지 기능

      • 1) SQL에 DISTINCT 추가
      • 2) 애플리케이션에서 중복 엔티티 제거
    • 예제로 살펴보기!

      • 위의 query문을 "select distinct t from Team t join fetch t.members"로 변경해보자.

      • 💥SQL에 distinct를 추가했지만 사실, 데이터가 다르므로 DB상에서는 중복 제거를 하지 못한다.

      • 💋(중요) But, 애플리케이션에서는 똑같은 식별자를 갖는 Team 엔티티를 제거한다.

        print:
        <팀A>에 속한 회원수 : 2
        <팀B>에 속한 회원수 : 1
  • 페치 조인과 일반 조인의 차이

    • 1) 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다.

      • JPQL은 결과를 반환할 때 연관관계 고려 X
      • 그저 SELECT 절에 지정한 엔티티만 조회
    • 2) 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)

      • 페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념
      • 연관된 엔티티를 함께 조회

페치 조인 2 - 한계

  • 한계

    • 💥페치 조인 대상에는 별칭을 줄 수 없다.

      • ex ) selelct t from Team t fetch join t.members as m (X)
    • 💥둘 이상의 컬렉션은 페치 조인 할 수 없다.

    • 💥컬렉션을 페치 조인하면 페이징 API (setFristResult, setMaxResults)를 사용할 수 없다.

      • XXXToOne과 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능

      • Hibernate는 경고 로그를 남기고 메모리에서 페이징(💥매우 치명적 위험)

      • select t from Team t fetch join t.memberselect m from Member m fetch join m.team ⇒ 일대다에서 다대일 페치 조인이 형성되기 때문에 페이징 API를 사용해도 된다.

      • OR, select t from Team t + **BETCHSIZE**설정

        • 방법 1) @을 사용해서 명시

          @BatchSize(size = 100)
          @OneToMany(mappedBy = "team")
          private List<Member> members = new ArrayList<>();
        • 방법 2) 설정 파일에 명시

          <properties>
          	<property name="hibernate.default_batch_fetch_size" value="100"/>
          </properties>
  • 페치 조인 특징

    • 연관된 엔티티들을 SQL 한 번으로 조회 ⇒ 성능 최적화
    • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
      • 글로벌 로딩 전략 : @OneToMany(fetch = FetchType.LAZY)
    • 실무에서 글로벌 로딩 전략은 모두 지연 로딩, BUT 최적화가 필요한 곳은 페치 조인 적용(특히, N + 1 문제는 해결)
  • 정리

    • 모든 것을 페치 조인으로 해결할 수는 없다.
    • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
    • 💋여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 결과를 내야 한다면? ⇒ 일반 조인을 사용하거나 필요한 데이터만 조회하는 DTO를 사용하여 반환하는 것이 효과적

다형성 쿼리

  • 테이블 모델

  • TYPE

    • 조회 대상을 특정 자신으로 한정 ex) Item 중에 Book, Movie를 조회해라
    • JPQL : select i from Item i where type(i) IN (Book, Movie)
    • SQL : select i from Item i where i.DTYPE in ('B', 'M')
  • TREAT

    • 자바의 타입 캐스팅과 유사
    • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용 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 : 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", member.getId())
        .getResultList();
    • 실행된 SQL은 두 경우가 똑같다.

      • select m.* from Member m where m.id = ?
  • 외래 키 값

    • 엔티티를 파라미터로 전달

      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은 두 경우가 똑같다.

      • select m.* from Member m where m.team_id = ?

Named 쿼리

  • 미리 정의(정적 쿼리)해서 이름을 부여해두고 사용하는 JPQL

  • @과 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 쿼리 환경에 따른 설정

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

벌크 연산

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

    • JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행
      • 1) 재고가 10개 미만인 상품을 리스트로 조회
      • 2) 상품 엔티티의 가격을 10% 증가
      • 3) 트랜잭션 커밋 시점에 변경 감지 동작
    • 변경된 데이터가 100건이면 100번의 UPDATE SQL 실행
  • 벌크 연산 : 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)

    • executeUpdate()의 결과는 영향받은 엔티티 수 반환

    • UPDATE, DELETE 지원

    • INSERT(insert into ... select, 하이버네이트 지원)

    • 모든 멤버의 나이를 20으로 설정

      //query 생성될때 자동 flush
      int resultCnt = em.createQuery("update Member m set m.age = 20")
                          .executeUpdate();
                  
      System.out.println("변경된 데이터 수  = " + resultCnt);
      
      System.out.println("회원 1의 나이 = " + member1.getAge());
      System.out.println("회원 2의 나이 = " + member2.getAge());
      System.out.println("회원 3의 나이 = " + member3.getAge());
      System.out.println("회원 4의 나이 = " + member4.getAge());
    • 출력

      print:
      변경된 데이터 수  = 4
      회원 1의 나이 = 0
      회원 2의 나이 = 0
      회원 3의 나이 = 0
      회원 4의 나이 = 0
    • DB

  • 벌크 연산 주의할 점

    • 벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리
    • 때문에, 벌크 연산을 먼저 실행거나 실행 후 영속성 컨텍스트 초기화하는 두 가지 방법
    • 🔔애플리케이션에서 조회하고 싶다면, 벌크 연산 이후 em.flush(), em.clear()
profile
열심히 해보자9999

0개의 댓글