12. 객체지향 쿼리 언어(3) - JPQL 조인


JPQL 조인

  • 내부 조인 (INNERT JOIN)
    •   String teamName = "teamA";
        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은 조인을 할때도 연관관계 필드를 사용함
      • m.team t
    •   String SQL = "SELECT M.ID, M.AGE ... FROM MEMBER M 
                      INNER JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
                      WHERE T.NAME = ?
                      "
        String JPQL = "select m FROM Member m INNER JOIN m.team t
                      WHERE t.name = ?"
      
  • 외부 조인 (OUTER JOIN)
    •   SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
      
    • OUTER는 생략 가능
  • 컬렉션 조인
    • 일대다 또는 다대다 관계처럼 컬렉션을 사용하는 곳에서 조인을 할 때
    •   // 컬렉션 값 연관 필드로 외부 조인
        SELECT t, m FROM Team t LEFT JOIN t.members m
      
  • 세타 조인
    • 전혀 관계없는 엔티티도 조인할 수 있음
    •   //JPQL
        SELECT COUNT(m) FROM Member m, Team t WHERE m.username = t.name
      
        //SQL
        SELECT COUNT(M.ID)
        FROM
            MEMBER M CROSS JOIN TEAM T
        WHERE
            M.USERNAME = T.NAME
      
  • JOIN ON 절(JPA 2.1)
    • 조인 대상을 필터링 할 때
    •   //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"
      

  • 페치(fetch) 조인
    • JPQL에서 성능 최적화를 위해 제공하는 기능
    • 연관된 엔티티나 컬렉션을 한 번에 같이 조인하는 기능
    • 동적으로 사용하는 즉시로딩인데 LAZY, EAGER와는 다르게 연관 엔티티도 하나의 쿼리로 함께 가져옴 (N+1 문제 해결)
    • 페치조인은 별칭(as) 사용이 불가능 (하이버네이트는 별칭 사용 지원 함)
    • join fetch 명령어로 사용
      • 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
    • 엔티티 페치 조인
      •   // 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
        
      • 회원과 팀을 지연로딩으로 설정헀어도 쿼리 결과는 프록시가 아닌 실제 엔티티가 됨
    • 컬렉션 페치조인
      •   // JPQL
          select m from Team t join fetch t.members where t.name="teamA"
        
          // SQL
          SELECT 
            T.*, M.*
          FROM TEAM T
          INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
          WHERE T.NAME="teamA"
        
      • 팀과 멤버는 일대다 관계, 위와 같이 컬렉션 조인을 할 경우 다음과 같은 결과
      • 1
      • 팀 테이블에서 “teamA”는 하나지만 조회 결과 2건으로 결과가 증가함
      • 이처럼 일대다 조인은 결과가 증가할 수 있음 (일대일, 다대일은 증가 안함)
    • 페치 조인과 DISTINCT
      • 일대다 조인을 할 때 결과가 늘어나는 상황에서 중복을 제거할 때 사용
      • 위에서 다대일 관계에서 컬렉션을 조회하면 조회 결과는 아래와 같이 여러개의 리스트가 반환됨
      • 2
      • 같은 Team 엔티티가 중복으로 반환된 상황
      • JPQL의 DISTINCT 명령어는 SQL에서도 DISTINCT를 처리하고 애플리케이션에서도 이런 상황에서 중복을 제거해줌
      •   select distinct t
          from Team t join fetch t.members
          where t.name = "teamA"
        
      • 3
      • SQL의 DISTINCT 결과는 왼쪽과 같이 차이가 없음(중복된 데이터가 없으므로)
      • 애플리케이션의 입장에서는 오른쪽과 같이 같은 식별자를 가진 중복 엔티티가 제거된 상태로 반환됨
    • 페치 조인과 일반 조인의 차이
      • 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음
      • JPQL은 결과를 반환할 때 연관관계를 고려하지 않음
      • 단지 SELECT 절에 지정한 엔티티만 조회함
        • 프록시나 아직 초기화되지 않은 컬렉션 래퍼를 반환, 후에 지연로딩으로 사용 시에 조회
      • 페치 조인은 연관 엔티티를 쿼리 시점에 조회하기 때문에 지연 로딩이 발생하지 않음
        • 따라서 준영속 상태에서도 객체 그래프 탐색이 가능함
      • 페치 조인은 연관된 엔티티도 함께 조회(즉시로딩)
      • 즉시로딩과는 또 다른게 SQL 한번에 연관된걸 다 조회할 수 있음
    • 페치 조인의 특징과 한계
      • 페치 조인 대상에는 별칭을 줄 수 없음
        • 하이버네이트는 지원하지만 사용하지 않을 것을 권장
        • 연속적인 페치 조인을 하는 경우가 아니면 사용해선 안됨
        • 왜 사용하면 안되냐?
          •   select t from Team t join fetch t.members m where m.age > 10
            
          • 이런 쿼리를 사용한다고 했을 때 m.age가 10 이상인 컬렉션만 반환됨
          • 연관된 모든 members 엔티티가 아닌 일부 엔티티만 가져오게 되고 만약 이게 2차 캐시랑 같이 사용되면 다른 엔티티가 members 연관관계를 조회해도 캐싱된 개수를 가져갈 것
          • 엔티티를 조회할 때 연관관계는 모두 가져오는 것이 객체 그래프 탐색의 사상?
      • 둘 이상의 컬렉션은 페치 조인 할 수 없음
        • 컬렉션 * 컬렉션은 Cartesian 곱.. 결과가 엄청 많아질 수 있기 때문에 제한
      • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없음
        • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능(DISTINCT 부분에서 설명했듯이 일대다 관계는 중복된 데이터가 발생하기 때문에)
        • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징함 (매우 위험..)

  • 벌크 연산
    • 엔티티를 수정하려면 영속성 컨텍스트의 변경감지 또는 병합을 사용
      • 만약 수백개 이상의 엔티티를 수정해야할 일이 생긴다면?
      • 변경감지로 엔티티 하나하나 업데으트를 하려면 많은 수의 쿼리 발생
      • 여러 건을 한번에 처리하는 쿼리를 벌크 연산이라고 함
    • executeUpdate() 메소드를 사용, 영향을 받은 엔티티의 수를 반환함
      •   // 재고가 10개 미만인 모든 상품의 가격을 10% 인상시키는 쿼리
          String qlSTring = 
              "update Product p " +
              "set p.price = p.price * 1.1 " +
              "where p.stockAmount < :stockAmount";
          int resultCount = em.createQuery(qlString)
                            .setParameter("stockAmount", 10)
                            .executeUpdate();
        
      • JPA 표준은 아니지만 하이버네이트는 INSERT 연산에 대해서도 벌크 연산 가능
    • 벌크 연산시 주의점
      • 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 하는 것
      • JPQL 처럼 쿼리 시 영속성 컨텍스트의 내용이 flush되지만 쿼리 후에 데이터베이스의 상태와 영속성 컨텍스트가 동기화되지 않음
        • 방법 1. 영속성 컨텍스트를 사용하기 전에 벌크 연산을 먼저 한다
        • 방법 2. 벌크 연산을 수행한 다음에는 영속성 컨텍스트를 초기화해준다
        • 방법 3. 벌크 연산 후에 em.refresh(entity)를 통해 엔티티를 다시 조회함
  • 영속성 컨텍스트와 JPQL
    • JPQL로 쿼리 후에 영속성 컨텍스트가 관리하는 것은 오직 엔티티 타입만!
      •   select m from Member m  // 엔티티 조회 - 영속성 컨텍스트에 관리됨
          select o.address from Order o // 임베디드 타입 조회 (관리 X)
          select m.id, m.username from Member m // 단순 필드 조회 (관리 X) 
        
      • 예를들어 임베디드 타입(o.address)를 조회하고 수정해도 변경 감지에 의한 수정은 되지 않음
    • 영속성 컨텍스트에 이미 있는 엔티티를 JPQL로 다시 조회한다면?
      • 조회 결과를 버리고 영속성 컨텍스트에 있는 엔티티를 반환함
    • find() vs JPQL
      • find()는 영속성 컨텍스트의 1차 캐시에 엔티티가 있으면 조회하지 않고 반환함
      • JPQL은 무조건 데이터베이스 쿼리, 1차 캐시에 엔티티가 있으면 반환, 없으면 1차 캐시로 영속성 컨텍스트에서 관리ㅑ





© 2020.02. by blupine