[JPA] 객체 지향 쿼리 언어 - 2

Lee Seung Jae·2021년 7월 25일
0

JPQL

다시한번 JPQL의 특징을 정리해보자

  • JPQL은 객체지향 쿼리 언어이다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
  • JPQL은 SQL을 추상화해서 특정 DB SQL에 의존하지 않는다.
  • JPQL은 결국 SQL로 변환된다.

기본 문법과 쿼리 API

JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다. em.persist()로 엔티티를 저장하므로 INSERT 쿼리는 없다.

JPQL 문법

select_문 :: =
    select_절
    from_절
    [where_절]
    [groupby_절]
    [having_절]
    [orderby_절]
    
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]

SELECT문

SELECT m FROM Member AS m where m.name = 'hello'
  • 대소문자 구분
    • 엔티티와 속성은 대소문자를 구분한다.
    • Member, name 은 대소문자를 구분함.
    • SELECT, FROM, AS 등.. JPQL 키워드는 대소문자를 구분하지 않음.
  • 엔티티 이름
    • JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다.
    • @Entity(name="") 로 지정할 수 있지만, 기본값은 클래스명을 기본값으로 한다.
  • 별칭 필수
    • 여기서는 m 이라는 별칭을 주었는데 JPQL은 별칭을 필수로 줘야한다. 아니면 에러 발생.

TypeQuery, Query

작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다. 쿼리 객체는 TypeQuery, Query가 있는데 반환할 타입을 명확하게 지정할 수 있다면 TypeQuery를, 그렇지 않으면 Query 객체를 사용하면 된다.

@Test
@DisplayName("TypeQuery 테스트")
void typeQueryTest() {
    TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

    List<Member> resultList = query.getResultList();

    assertThat(resultList.get(0).getName()).isEqualTo(member.getName());
}

em.createQuery()부분의 두번째 매개변수에 반환 타입을 위와같이 지정하면 TypeQuery를 반환, 아니면 Query를 반환한다.

결과조회

  • query.getResultList() : 결과를 리스트로 반환, 결과가 없으면 빈 컬렉션을 반환한다.
  • query.getSingleResult() : 결과가 하나일때 사용
    • 결과가 없으면 NoResultException 예외 발생
    • 결과가 1개보다 많으면 NonUniqueResultException 발생

파라미터 바인딩

JDBC와의 차이점

JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩 지원

  • 이름 기준 파라미터
    • 이름 기준 파라미터는 파라미터를 이름으로 구분하는 방법이다.
    • 앞에 : 를 사용한다.

예제

@Test
@DisplayName("이름기준 파라미터")
void nameOfParameterTest() {
    String name = "kim";

    TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m where m.name = :name", Member.class);

    query.setParameter("name", name);
    List<Member> resultList = query.getResultList();
    assertThat(resultList.get(0).getName()).isEqualTo(member.getName());
}

이렇게도 할 수 있고
메소드 체이닝 방식으로도 할 수 있다.

List<Member> resultList = em.createQuery("SELECT m FROM Member m where m.name = :name", Member.class)
                            .setParameter("name", name);
                            .getResultList();
  • 위치 기준 파라미터
    • 위치 기준은 사용하려면 ? 다음에 위치 값을 주면 된다.
    • 위치 값은 1부터 시작이다.
em.createQuery("SELECT m FROM Member m where m.name= ?1", Member.class);

이런식으로 사용하면 되겠다. 나머지는 이름 기준과 같다.

위치 기준 보다는 이름 기준 파라미터 바인딩 방식이 더 명확하다.

프로젝션

SELECT절에 조회할 대상을 지정하는 것을 프로젝션이라고 하고 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다.

엔티티 프로젝션

SELECT m FROM Member m      //회원
SELECT m.team From Member m //팀

처음은 회원을 조회했고 두번째는 회원과 연관된 팀을 조회했는데 엔티티를 프로젝션 대상으로 한 예시이다.
이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다❗❗

임베디드 타입 프로젝션

JPQL에서 임베디드 타입은 엔티티와 비슷하게 사용 되는데 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.

주가 되는 엔티티에서부터 시작해서 나가야 쿼리가 수행된다.
임베디드 타입은 엔티티 타입이 아니라 값 타입이다.
따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.

스칼라 타입 프로젝션

숫자, 문자, 날짜와 같은 기본 데이터 타입이 스칼라 타입.
반환되는 값의 Wrapper클래스 매칭하여 추출

중복 데이터를 제거하려면 DISTINCT를 사용한다.
통계형 쿼리는 지금 다루지 않지만, 통계용 쿼리도 스칼라 타입이다.

여러 값 조회

엔티티 대상이 조회하기 편리하지만, 특정 데이터만 선택해서 추출해야 하는 경우에는 Query클래스 객체를 사용해야한다.

페이징 API

페이징 처리용 SQL을 작성하는 일은 지루하고 반복적이다. 더 큰 문제는 DB마다 페이징을 처리하는 SQL 문법이 다르다.
JPA는 페이징을 두개의 API로 추상화했다.

  • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터의 수

DB마다 다른 페이징 처리를 같은 API로 처리할 수 있는 이유는 DB 방언 덕분이다. JPQL이 방언에 따라 설정에 맞는 DB SQL로 변환된다.

나는 PostgreSQL을 사용해서 결과는 아래와 같이 나왔다.

@Test
@DisplayName("페이징 API")
void pagingApiTest() {
    TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m order by m.name desc", Member.class);

    query.setFirstResult(10);
    query.setMaxResults(20);
    query.getResultList();
}

집합과 정렬

집합은 집합 함수와 함께 통계 정보를 구할때 사용함.

집합 함수

함수설명
COUNT결과 수를 구한다. 반환 타입: Long
MAX, MIN최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용한다.
AVG평균 값을 구한다. 숫자타입만 사용이 가능하다. 반환 타입: Double
SUM합을 구한다. 숫자타입만 사용할 수 있다. 반환 타입: 정수합: Long, 소수합: Double, BigInteger합: BigInteger, BigDecimal합: BigDecimal

고려사항

  • Null 값은 무시하므로 통계에 잡히지 않는다.
  • 값이 없는데 AVG, MIN, MAX, SUM 을 사용하면 Null 값이 된다. COUNT는 0
  • DISTINCT를 집합함수 안에 사용하여 중복된 값 제거 후 집합을 구할 수 있다.
  • DISTINCT를 COUNT에서 사용할 때 임베디드 타입 지원 ❌

Group By, Having

Group By는 통계 데이터를 구할 때 특정 그룹을 묶어준다.
Having은 Group By와 같이 사용하는데 Group By가 끝나고 난 뒤에 필터링해준다.

Order By

order by는 정렬할 때 사용한다.

  • ASC : 기본값, 오름차순
  • DESC : 내림차순

JPQL 조인

JPQL도 조인을 지원한다. SQL 조인과 기능은 같은데 문법이 조금 다르다.

JPQL조인을 SQL 조인처럼 사용하면 문법 오류가 발생한다.
반드시 JOIN 명령어 다음에 조인할 객체의 연관 필드를 사용해야 한다.

SELECT m.username, t.name
FROM Member m JOIN m.team t
WHERE t.name = 'team1'

이렇게가 아니라 WHERE m.팀과관련된 것 이 나와야 한다.

내부조인

Inner Join을 사용한다. Inner는 생략이 가능하다.

외부조인

외부 조인은 기능상 SQL의 외부 조인과 같다. OUTER는 생략이 가능하기 때문에 LEFT JOIN 으로 사용한다.

컬렉션 조인

1:N, N:1 처럼 컬렉션을 사용하는 곳에 조인하는 것이 컬렉션 조인이다.

세타 조인

WHERE절을 이용해 세타 조인을 할 수 있다.
세타 조인은 내부 조인만 지원
select count(m) from Member m, Team t where m.name = t.name
관계없는 엔티티를 조인할 수 있다.

JOIN ON절

ON 절을 사용하면 조인 대상을 필터링 후 조인이 가능하다. Inner Join에서의 ON 절은 WHERE절을 사용할 때와 같아서 외부 조인에서만 사용한다.

페치 조인

JPQL에서 성능 최적화를 위해 제공하는 기능이다.
join fetch로 사용할 수 있다.

엔티티 페치 조인

select m from Member m join fetch m.team

join뒤에 fetch를 붙이면 연관된 엔티티나 컬렉션을 함께 조회한다.
m.team 다음에 별칭을 붙이는데 페치 조인은 별칭을 사용할 수 없다.

Member와 Team을 지연로딩으로 설정했다고 한다면 회원을 조회할 때 페치 조인을 사용해서 팀도 같이 조회를해서 팀 엔티티는 프록시가 아닌 실제 엔티티이다. 그래서 연관된 팀을 사용해도 지연로딩이 일어나지 않는다.
프록시가 아닌 실제 엔티티기 때문에 멤버 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태여도 연관된 팀을 조회할 수가 있다.

컬렉션 페치 조인

컬렉션으로 페치 조인한 JPQL은 조회하는 엔티티가 어떤 엔티티안에 연관되어있다면, 연관된 다른 엔티티도 같이 조회한다. 그래서 조인하면서 결과가 증가하여 조회를 두번하게 된다.

페치조인과 DISTINCT

SQL의 DISTINCT는 중복된 결과를 제거하는 명령이다. JPQL에서의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것과 더불어 애플리케이션에서 한번 더 중복을 제거한다.

JPQL에서 select distinct의 의미는 엔티티의 중복을 제거하라는 뜻이므로 위에서 조회를 두번했던 결과를 하나만 조회하게 된다.

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

JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다. 그래서 연관된 엔티티는 조회하지 않는다.
컬렉션을 지연 로딩으로 설정한다면 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환한다.
즉시 로딩 이라면 컬렉션을 즉시 로딩하기 위해 바로 쿼리를 한번 더 실행한다.

페치 조인의 특징과 한계

페치 조인을 사용하면 SQL한번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다.
엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라고 한다.
페치 조인은 글로벌 로딩 전략보다 우선한다. 지연 로딩을 설정해도 JPQL에서 페치 조인을 사용하면 페치 조인이 적용되어 같이 조회하게 된다.
최적화를 위해서 즉시 로딩을 글로벌 로딩 전략으로 가져간다면 항상 즉시 로딩이 일어나게 된다.
일부는 빠를 수 있지만 사용하지 않는 엔티티들도 자주 로딩하여 속도 저하가 우려된다.
그래서

글로벌 로딩 전략은 지연 로딩으로 채택하고 최적화가 필요할 때는 JPQL에서 페치 조인을 적용하는 것이 효과적이다.

페치 조인을 사용하면 연관된 엔티티를 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않는다. 따라서 준영속 상태에서도 객체 그래프를 탐색할 수 있다

페치조인의 한계점

  • 페치 조인 대상에는 별칭을 줄 수 없다.
  • 둘 이상의 컬렉션을 페치할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
    • 컬렉션이 아닌 단일 값 연관 필드들은 페치 조인을 사용해도 페이징 API 사용 가능

페치 조인은 SQL한번으로 연관된 여러 엔티티를 조회할 수가 있어서 성능 최적화에 상당히 유용하지만 모든 것을 해결할 수는 없다. 그래서 여러 테이블을 조인해서 원하는 값들만 추출해야 할 경우에는 페치 조인보다는 여러 테이블에서 필요한 값들만 조회 한 후에 DTO로 반환 해주는 것이 더 효과적일 수가 있다.

경로 표현식

경로 표현식은 .(점)을 통해 객체 그래프를 탐색하는 것이다.

  • 상태 필드 : 단순히 값을 저장하기 위한 필드
  • 연관 필드 : 연관관계를 위한 필드, 임베디드 타입 포함
    • 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티
    • 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션

경로 표현식과 특징

경로 표현식을 사용하여 경로 탐색을 하려면 3가지 경로에 따라 어떤 특징이 있는지 보자

  • 상태 필드 경로 : 경로 탐색의 끝 더는 탐색 할 수 없음.
  • 단일 값 연관 경로 : 묵시적으로 내부 조인이 일어남, 단일 값 연관 경로는 계속 탐색할 수 있다.
  • 컬렉션 값 연관 경로 : 묵시적으로 내부 조인이 일어남, 더는 탐색할 수 없다.
    • 단, FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색이 가능함.

단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이것을 묵시적 조인 이라고 한다.
묵시적 조인은 모두 내부 조인이다.

  • 명시적 조인 : JOIN을 직접 적어주는 것
  • 묵시적 조인 : 경로 표현식에 의해서 묵시적으로 조인이 일어 나는 것, INNER JOIN만 할 수 있음.

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

경로 탐색을 사용하면 묵시적 조인이 발생해서 SQL에서 내부 조인이 일어날 수 있다.

  • 항상 내부조인이다.
  • 컬렉션은 경로탐색의 끝, 컬렉션에서 경로 탐색을 하려면 명시적으로 조인을 해서 별칭을 얻어야만 한다.
    • ex) select m.name from Team t join t.members m
  • 경로 탐색은 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM절에 영향을 준다.

조인이 성능으로 차지하는 부분이 아주 크다. 묵시적 조인의 단점은 조인이 일어나는 상황을 한눈에 파악하기 어렵다는 단점이 있다. 성능이 중요하다면 분석하기 쉽도록 명시적 조인을 사용하는것이 좋다.

profile
💻 많이 짜보고 많이 경험해보자 https://lsj8367.tistory.com/ 블로그 주소 옮김

0개의 댓글