[ 김영한 자바 ORM 표준 JPA 프로그래밍 - 기본편 #10 ] JPQL 중급 문법

김수호·2024년 5월 11일
0
post-thumbnail

✔️ JPQL - 페치 조인(fetch join)

  • SQL 조인 종류는 아니다.
  • JPQL 에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • join fetch 명령어 사용
  • 페치 조인: [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로

엔티티 페치 조인

  • 회원을 조회하면서 연관된 팀도 함께 조회 (SQL 한 번에)
  • SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 조회
    • 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 만 적었는데, 실제 실행된 SQL 에는 M과 T가 모두 나열된 것을 확인할 수 있다. ( 마치 즉시로딩으로 하나 조회할 때와 같다. 단지 이것은 쿼리에서 내가 직접 명시적으로 정하는 것이다. )
      • 따라서 지연로딩으로 설정되었더라도, fetch join 이 먼저 적용되기 때문에, Team 에는 프록시 객체가 아닌 실제 엔티티 객체가 주입된다. (영속성 컨텍스트에도 다 반영됨)

컬렉션 페치 조인

  • 일대다 관계, 컬렉션 페치 조인
  • 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'
  • 주의) 컬렉션 페치 조인의 결과에 주의하자. ( 일대다 관계 조인의 데이터 뻥튀기 )
    • ex) select t from Team t join fetch t.members
    • 만약 Team 에는 팀A 와 팀B, 두 건의 데이터가 저장되어있고, 팀A에 해당하는 Member 는 회원1, 회원2. 팀 B에 해당하는 Member 는 회원3이라 가정하자.
    • 그러면 위 결과는 총 3개가 반환되며 아래와 같다.
      • 참고) 테이블 설계는 TEAM(ID, NAME) MEMBER(ID, TEAM_ID, NAME) 으로 되어있고, 엔티티 설계시 연관관계는 양방향으로 하였다.
      • 결과를 보면 팀A 정보가 동일하게 2건이 나온다는 점에 주의하자.
        • 참고로 영속성 컨텍스트에는 팀A 1건, 팀A 에 해당하는 회원 2건이 반영된다.
      • 여기서 팀A의 컬렉션 중복이 싫다면? (아래 참고)

페치 조인과 DISTINCT

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령
    • 참고) SQL의 DISTINCT 만으로는 중복을 다 제거할 수 없음.
  • JPQL의 DISTINCT의 2가지 기능
    • SQL에 DISTINCT를 추가 + 애플리케이션에서 엔티티 중복 제거
    • ex) JPQL: select distinct t from Team t join fetch t.members where t.name = '팀A'
      • 1) SQL에 DISTINCT를 추가
        • SQL에 DISTINCT를 추가하더라도 데이터가 다르므로(회원1,회원2) 중복제거 실패
      • 2) 애플리케이션에서 중복 제거를 시도한다. ( 같은 식별자를 가진 Team 엔티티 제거 )
        • 중복이 제거된 결과 리스트가 반환된다. ( 애플리케이션으로 쿼리 결과가 올라올 때 JPA가 한 번더 걸러주는 것 )

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

  • 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음
    • JPQL: select t from Team t join t.members 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'
      • 팀 엔티티 정보만 조회됨. (회원 엔티티 조회 X)
      • JPQL 은 결과를 반환할 때 연관관계를 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐
  • 페치 조인 실행시 연관된 엔티티를 함께 조회
    • 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'
      • 페치 조인을 사용할 때는(사용할 때만) 연관된 엔티티도 함께 조회(사실상 즉시 로딩이 일어난다고 보면된다.)
      • 페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념

 

페치 조인 - 특징 & 한계

  • 특징
    • 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
    • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
      • ex) @OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
    • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
    • 최적화가 필요한 곳은 페치 조인 적용
  • 한계
    • 페치 조인 대상에는 별칭을 줄 수 없다. (별칭을 주지 않는 것이 관례)
      • ex) select t from Team t join fetch t.members m
        • 별칭을 준 m 을 가지고 where 조건에 m.username 등으로 필터링하고, 등등 이렇게 쓰지 말아야한다. 그렇게 하고싶으면 차라리 따로 조회해야 한다. ( 페치 조인은 기본적으로 "나와 연관된 것을 모두 가져오는 것" )
          • 잘못 필터링해서 조작하게 되면, 원치않는 방향으로 동작할 수 있다. (데이터 정합성이 맞지 않는 등)
        • 참고) 별칭을 쓰는 경우가 있긴한데, 그 경우는 연속적인 fetch join 를 작성해야 하는 경우에 해당한다.
          • ex) select o from Order o join fetch o.orderItems oi join fetch oi.item i
      • 하이버네이트는 가능, 가급적 사용 X
    • 둘 이상의 컬렉션은 페치 조인할 수 없다.
      • 페치 조인 컬렉션은 하나만 지정할 수 있다.
      • 컬렉션 페치 조인은 1개만 사용할 수 있다. 컬렉션 둘 이상에 페치 조인을 사용하면 안된다. 데이터가 부정합하게 조회될 수 있다.
    • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResult)를 사용할 수 없다.
      • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
      • 하이버네이트는 경로 로그를 남기고 메모리에서 페이징 (매우 위험)
        • firstResult/maxResults specified with collection fetch; applying in memory

페치 조인 정리

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

 

✔️ JPQL - 기타

  • 엔티티 직접 사용
    • 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
    • 참고) 엔티티 직접 사용 - 엔티티를 파라미터로 전달
      • JPQL
        • select m from Member m where m = :member
        • 로직: ... setParameter("member", member);
      • 실행된 SQL
        • select m.* from Member m where m.id = ?
    • 참고) 엔티티 직접 사용 - 외래키 값
      • JPQL
        • select m from Member m where m.team = :team
        • 로직: ... setParameter("team", team);
      • 실행된 SQL
        • select m.* from Member m where m.team_id = ?
  • Named 쿼리
    • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
    • 정적 쿼리
    • 어노테이션, XML에 정의
    • 애플리케이션 로딩 시점에 초기화 후 재사용
    • 애플리케이션 로딩 시점에 쿼리를 검증
    • ex)
      • 정의 - 어노테이션
        • @NamedQuery(name = "Member.findByUsername", query = "select m from Member m where m.username = :username" )
        • 참고) name 관례: 엔티티명.쿼리이름
      • 사용
        • em.createNamedQuery("Member.findByUsername", Member.class).setParameter("username", "회원A").getResultList();
  • 벌크 연산
    • 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
    • executeUpdate() 의 결과는 영향받은 엔티티 수 반환
      • em.createQuery("update Product p set p.price = p.price * 1.1 where p.stockAmount < :stockAmount").setParameter("stockAmount", 10).executeUpdate();
    • UPDATE, DELETE 지원
    • INSERT(insert into .. select, 하이버네이트 지원)
    • 주의) 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 바로 직접 쿼리 ( 따라서 잘못하면 꼬이기 때문에, 아래와 같이 처리하자. )
      • (벌크 연산을 애초에 먼저 실행) or (벌크 연산 수행 후 영속성 컨텍스트 초기화)

강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 김영한 강사님께 있습니다.

profile
현실에서 한 발자국

0개의 댓글