ORM JPA5

유요한·2023년 12월 1일
0

JPA

목록 보기
5/10
post-thumbnail

객체지향 쿼리 언어(JPQL)

JPA는 다양한 쿼리 방법을 지원

  • JPQL
  • JPA Criteria
  • QueryDSL
  • 네이티브 SQL

JPQL는 가장 단순한 조회 방법입니다.

  • EntityManager.find()
  • 객체 그래프 탐색(a.getB().getC())

JPQL

  • JPA를 사용하면 엔티티 객체를 중심으로 개발

  • 문제는 검색 쿼리인데 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색

  • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능

  • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함한 SQL이 필요

  • JPA는 SQL을 추상화한 JPQL이라는 객체지향 쿼리 언어 제공

  • SQL과 문법 유사

    select, from, where, group by, having, join 지원

  • JPQL은 엔티티 객체를 대상으로 쿼리

  • SQL은 데이터베이스 테이블을 대상으로 쿼리

  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존x

  • JPQL을 한마디로 정의하면 객체 지향 SQL

QueryDSL 소개

  • 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
  • JPQL 빌더 역할
  • 컴파일 시점에 문법 오류를 찾을 수 있음
  • 동적쿼리 작성 편리함
  • 단순하고 쉬움
  • 실무 사용 권장

실무에서는 JPA를 사용할 때 동적 쿼리를 사용할 일이 많은데 그럴 때 QueryDSL을 사용하면 된다.


JPQL 소개

  • JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리를 하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.

  • JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.

  • JPQL은 결국 SQL로 변환된다.

JPQL 문법

  • select m from Member as m where m.age > 18
  • 엔티티와 속성은 대소문자 구분O

    Member, age

  • JPQL 키워드는 대소문자 구분X

    select, from, where

  • 엔티티 이름 사용, 테이블 이름이 아님
  • 별칭은 필수(m)

    as는 생략 가능

TypeQuery, Query

  • TypeQuery
    반환 타입이 명확할 때 사용

  • Query
    반환 타입이 명확하지 않을 때 사용

JPQL 타입 표현

JPQL 기타

조건식 - case 식

JPQL 경로 표현식


프로젝션

  • SELECT 절에 조회할 대상을 지정하는 것

  • 프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)

  • SELECT m FROM Member m -> 엔티티 프로젝션

  • SELECT m.team FROM Member m -> 엔티티 프로젝션

  • SELECT m.address FROM Member m -> 임베디드 타입 프로젝션

  • SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션

  • DISTINCT로 중복 제거

여러 값 조회

  • SELECT m.username, m.age FROM Member m

    1. Query 타입으로 조회
    1. Object[] 타입으로 조회
    1. new 명령어로 조회
      • 단순 값을 DTO로 바로 조회
        SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM
        Member m
      • 패키지 명을 포함한 전체 클래스명 입력
      • 순서와 타입이 일치하는 생성자 필요

페이징 API

  • JPA는 페이징을 다음 두 API로 추상화
    • setFirstResult(int startPosition) : 조회 시작 위치
      (0부터 시작)

    • setMaxResults(int maxResult) : 조회할 데이터 수


조인


서브 쿼리

하이버네이트6 부터는 FROM 절의 서브쿼리를 지원합니다.


JPQL 페치 조인(fetch join)


JPQL - 다형성 쿼리


JPQL - 엔티티 직접 사용


JQPL - Named 쿼리


JPQL - 벌크 연산


경로 표현식 특징

  • 상태 필드(state field) : 경로 탐색의 끝, 탐색x
  • 단일 값 연관 경로 : 묵시적 내부 조인(inner join) 발생, 탐색O
  • 컬렉션 값 연관 경로 : 묵시적 내부 조인 발생, 탐색x

    form 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능

실무에서는 묵시적 조인을 사용하지말고 명시적 조인을 사용하는것이 좋다.

명시적 조인, 묵시적 조인

  • 명시적 조인 : join 키워드 직접 사용

    select m from Member m join m.team t

  • 묵시적 조인 : 경로 표현식에 의헤 묵시적으로 SQL 조인 발생 (내부조인만 가능)

    select m.team from Member m

경로 탐색을 사용한 묵시적 조인 시 주의사항

  • 항상 내부 조인
  • 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야함
  • 경로 탐색은 주로 select, where 절에서 사용하지만 묵시적 조인으로 인해 SQL의 from (join)절에 영향을 줌

페치 조인(fetch join)

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

엔티티 페치 조인

JPA를 사용하다 보면 의도하지 않았지만 여러 번의 select 문이 순식간에 여러 개가 나가는 현상을 본 적이 있을 것이다. 이러한 현상을 N+1문제라고 부른다.

N+1 문제란?

연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 현상

발생 이유

N+1 문제가 발생하는 이유는 JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 Fetch 전략을 참고하지 않고 오직 JPQL 자체만을 사용한다. 즉, 아래와 같은 순서로 동작한다.

  • Fetch 전략이 즉시 로딩인 경우
  1. findAll()을 한 순간 select t from Team t 이라는 JPQL 구문이 생성되고 해당 구문을 분석한 select * from team 이라는 SQL이 생성되어 실행된다. ( SQL 로그 중 Hibernate: select team0.id as id1_0, team0.name as name2_0 from team team0_ 부분 )
  2. DB의 결과를 받아 team 엔티티의 인스턴스들을 생성한다.
  3. team과 연관되어 있는 user 도 로딩을 해야 한다.
  4. 영속성 컨텍스트에서 연관된 user가 있는지 확인한다.
  5. 영속성 컨텍스트에 없다면 2에서 만들어진 team 인스턴스들 개수에 맞게 select * from user where team_id = ? 이라는 SQL 구문이 생성된다. ( N+1 발생 )
  • Fetch 전략이 지연 로딩인 경우
  1. findAll()을 한 순간 select t from Team t 이라는 JPQL 구문이 생성되고 해당 구문을 분석한 select * from team 이라는 SQL이 생성되어 실행된다. ( SQL 로그 중 Hibernate: select team0.id as id1_0, team0.name as name2_0 from team team0_ 부분 )
  2. DB의 결과를 받아 team 엔티티의 인스턴스들을 생성한다.
  3. 코드 중에서 team 의 user 객체를 사용하려고 하는 시점에 영속성 컨텍스트에서 연관된 user가 있는지 확인한다
  4. 영속성 컨텍스트에 없다면 2에서 만들어진 team 인스턴스들 개수에 맞게 select * from user where team_id = ? 이라는 SQL 구문이 생성된다. ( N+1 발생 )

N + 1 문제가 발생할 경우 fetch join으로 해결해야 한다.

JPQL을 사용하여 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 같이 가져오게 하는 방법이다.

  // N +1 문제를 해결하기 위해서는 join fetch를 사용한다.
  // 연관된 데이터를 가지고 오려면 join을 해야하고
  // fetch는 한번에 가지고 온다는 것이다.
  String query = "select m From Member m join fetch m.team";
            
  // 여기서 List에 담기는 값은 프록시가 아니라 엔티티의 값이다.
 List<Member> resultList = 
 entityManager.createQuery(query, Member.class).getResultList();

여러번 실행하지 않고 한번에 실행되는 것을 볼 수 있다.

컬렉션 페치 조인

  String query = "select t From Team t join fetch t.members";

            // 여기서 List에 담기는 값은 프록시가 아니라 엔티티의 값이다.
            List<Team> resultList = entityManager.createQuery(query, Team.class).getResultList();

            for (Team team : resultList) {
                log.info("team : " + team.getName() + "\n members : " +team.getMembers().toString());
        }

이렇게 실행하면 2번 실행되는 것을 볼 수 있는데 이유는 다음과 같다.

지금 팀A에 member가 2명이 있기때문에 일대다인데 멤버가 2명이기 때문에 2번 뜨는 것이다.

페치 조인과 DISTINCT

  • SQL과 DISTINCT는 중복된 결과를 제거하는 명령
  • JPQL의 DISTINCT 2가지 기능 제공
    1. SQL에 DISTINCT를 추가
    2. 애플리케이션에서 엔티티 중복 제거

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

  • JPQL은 결과를 반환할 때 연관관계 고려X
  • 단지 SELECT 절에 지정한 엔티티만 조회할 뿐
  • 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회X
  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

페치 조인 실행 예시

페치 조인의 특징과 한계

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

    하이버네이트는 가능, 가급적 사용X

  • 둘 이상의 컬렉션은 페치 조인 할 수 없다.

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

    • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
  • 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화

  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함

    @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략

  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩

  • 최적화가 필요한 곳은 페치 조인 적용

페치 조인 - 정리

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

JPQL - 다형성 쿼리

TYPE

TREAT(JPA 2.1)

  • 자바의 타입 캐스팅과 유사
  • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
  • FROM, WHERE, SELECT(하이버네이트 지원) 사용
  • 예) 부모인 Item과 자식 Book이 있다.


JPQL - 엔티티 직접 사용


JPQL - Named 쿼리

Named 쿼리 - 정적 쿼리

  • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
  • 정적 쿼리
  • 어노테이션, XML에 정의
  • 애플리케이션 로딩 시점에 초기화 후 재사용
  • 애플리케이션 로딩 시점에 쿼리를 검증

Named 쿼리 - 어노테이션

Named 쿼리 - XML에 정의

Named 쿼리 환경에 따른 설정

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

JPQL - 벌크 연산

벌크 연산

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

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

예제

벌크 연산 주의

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리

  • 벌크 연산을 먼저 실행
  • 벌크 연산 수행 후 영속성 컨텍스트 초기화
profile
발전하기 위한 공부

0개의 댓글