본문 바로가기
spring/jpa

자바 ORM 표준 JPA 프로그래밍 - 10장 객체지향 쿼리 언어

by 쭈꾸마뇽 2021. 7. 24.

객체지향 쿼리

EntityManager.find() 메소드를 사용하면 식별자로 엔티티 하나를 조회할 수 있다. 하지만 이 기능만으로 애플리케이션을 개발하기는 어렵다.

JPQL

JPQL은 엔티티 객체를 조회하는 객체지향 쿼리이다.  문법은 SQL과 미슷하고 ANSI 표준 SQL이 제공하는 기능을 유사하게 지원한다.  JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다.  그리고 데이터베이스 Dialect만 변경하면 JPQL을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있다.

이번 게시글에서 사용할 도메인 모델

@Test
public void test01() {
    TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);

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

    resultList.forEach(System.out::println);
}

  • 대소문자 구분 -> 엔티티와 속성은 대소문자를 구분한다.
  • 엔티티 이름 -> JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다.
  • 별칭 필수 -> Member m을 보면 Member에 m이라는 별칭을 주었다.  JPQL에서는 별칭을 필수로 사용해야 한다.

TypedQuery, Query

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

TypedQuery : 위의 코드 확인

Query :

@Test
public void test02() {
    Query query = em.createQuery("select m.name, m.age from Member m");

    List resultList = query.getResultList();

    for (Object o : resultList) {
        Object[] result = (Object[]) o;
        System.out.println(result[0]);
        System.out.println(result[1]);
    }
}

결과 조회

  • query.getResultList(): 결과를 예제로 반환한다.  만약 결과가 없으면 빈 컬렉션을 반환한다.
  • query.getSingleResult(): 결과가 정확히 하나일 때 사용한다.  결과가 없으면 NoResultException 예외가 발생하고 결과가 1개보다 많으면 NonUniqueResultException 예외가 발생한다.

파라미터 바인딩

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

@Test
public void test03() {
    String nameParam = "name1";
    TypedQuery<Member> query = em.createQuery("select m from Member m where m.name = :name", Member.class);
    query.setParameter("name", nameParam);

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

    resultList.forEach(System.out::println);
}

@Test
public void test04() {
    String nameParam = "name1";
    TypedQuery<Member> query = em.createQuery("select m from Member m where m.name = ?1", Member.class);
    query.setParameter(1, nameParam);

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

    resultList.forEach(System.out::println);
}

프로젝션

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

@Test
public void test05() {
    Query query = em.createQuery("select m.name from Member m");

    List resultList = query.getResultList();

    for (Object o : resultList) {
    System.out.println(o);
    }
}

@Test
public void test06() {
    Query query = em.createQuery("select o.member, o.product from Orders o");

    List<Object[]> resultList = query.getResultList();

    for (Object[] o : resultList) {
    System.out.println(o[0]);
    System.out.println(o[1]);
    }
}

New 명령어

위 예제는 필드로 프로젝션해서 타입을 지정할 수 없어서 TypedQuery를 사용할 수 없다.  따라서 Object[]를 반환받았는데 Dto조회를 하면 가능하다.

public class UserDto {
    private String name;
    private int age;

    public UserDto() {
    }

    public UserDto(String name, int age) {
        this.name = name;
        this.age = age;
    }
}


@Test
public void test07() {
    TypedQuery<UserDto> query = em.createQuery("select new learn.jpa.model.UserDto(m.name, m.age) from Member m", UserDto.class);

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

    resultList.forEach(System.out::println);
}

페이징 API

  • setFirstResult(int startPosition): 조회 시작 위치(0부터 시작)
  • setMaxResults(int maxResult): 조회할 데이터 수
@Test
public void test08() {
    TypedQuery<UserDto> query = em.createQuery("select new learn.jpa.model.UserDto(m.name, m.age) from Member m", UserDto.class);
    query.setFirstResult(1);
    query.setMaxResults(1);

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

    resultList.forEach(System.out::println);
}

집합과 정렬

함수 설명
COUNT 결과 수를 구한다. Long
MAX, MIN 최대, 최소 값을 구한다
AVG 평균을 구한다. Double
SUM 합을 구한다. 숫자 타입만 사용한다

주의 사항

  • Null 값은 무시하므로 통계에 잡히지 않는다
  • 만약 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL값이 된다. 단 COUNT = 1
  • DISTINCT를 집합함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다.
  • DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다
@Test
public void test09() {
    Query query = em.createQuery("select count(m), sum(m.age), avg(m.age) from Member m");

    List<Object[]> resultList = query.getResultList();

    for (Object[] o : resultList) {
        System.out.println("Count : " + o[0]);
        System.out.println("Sum : " + o[1]);
        System.out.println("Avg : " + o[2]);
    }
}

GROUP BY, HAVING

GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다.

HAVING은 GROUP BY와 함께 사용하는데 GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링한다.

@Test
public void test10() {
    Query query = em.createQuery("select  count(m), sum(m.age), avg(m.age) from Member m left join m.team t group by t.name having avg(m.age) >= 10");

    List<Object[]> resultList = query.getResultList();

    for (Object[] o : resultList) {
        System.out.println("Count : " + o[0]);
        System.out.println("Sum : " + o[1]);
        System.out.println("Avg : " + o[2]);
    }
}

JPQL 조인

JPQL도 조인을 지원하는데 SQL 조인과 기능은 같고 문법만 약간 다르다.

  • 내부 조인
@Test
public void test12() {
    String teamName = "team1";
    TypedQuery<Member> query = em.createQuery("select m from Member m inner join m.team t where t.name = :teamName", Member.class);
    query.setParameter("teamName", teamName);

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

    resultList.forEach(System.out::println);
}
  • 외부 조인
@Test
public void test13() {
    String teamName = "team1";
    TypedQuery<Member> query = em.createQuery("select m from Member m left join m.team t where t.name = :teamName", Member.class);
    query.setParameter("teamName", teamName);

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

    resultList.forEach(System.out::println);
}

JPQL 내부 조인 구문을 보면 SQL의 조인과 약간 다르다.  JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다.  여기서 m.team이 연관 필드인데 연관 필드는 다른 엔티티와의 연관관계를 가지기 위해 사용하는 필드를 말한다.

  • 컬렉션 조인
@Test
public void test14() {
    TypedQuery<Team> query = em.createQuery("select t from Team t left join t.members m", Team.class);

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

    resultList.forEach(System.out::println);
}

  • 세타 조인
@Test
public void test15() {
    TypedQuery<Member> query = em.createQuery("select m from Member m, Team t where m.name = t.name", Member.class);

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

    resultList.forEach(System.out::println);
}

  • 페치 조인
@Test
public void test16() {
    TypedQuery<Member> query = em.createQuery("select m from Member m join fetch m.team", Member.class);

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

    resultList.forEach(System.out::println);
}

페치 조인은 SQL에서 이야기하는 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다.  이것은 견관된 엔티티나 컬렉션을 한번에 같이 조회한다.

예제를 보면 join 다음에 fetch라 적었다.  이렇게 하면 회원과 팀을 함께 조회한다.  참고로 일반적인 JPQL 조인과는 다르게 m.team 다음에 별칭이 없는데 페치 조인은 별칭을 사용할 수 없다.

실행된 쿼리를 보면 JPQL에서 select m으로 회원 엔티티만 선택했는데 실행된 SQL을 보면 연관된 팀도 함께 조회된 것을 확인할 수 있다.  그리고 그래프 탐색으로 m.team을 조회해도 지연로딩이 발생하지 않는다.

  • 컬렉션 페치 조인
@Test
public void test18() {
    TypedQuery<Team> query = em.createQuery("select distinct t from Team t join fetch t.members", Team.class);

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

    resultList.forEach(System.out::println);
}

  • 페치 조인과 DISTINCT

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

위 컬렉션 페치 조인 예제의 결과를 확인해보면 ID = 1인 TEAM이 MEMER 수만큼 중복되서 조회된다.

@Test
public void test18() {
    TypedQuery<Team> query = em.createQuery("select distinct t from Team t join fetch t.members", Team.class);

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

    resultList.forEach(System.out::println);
}

DISTINCT를 사용하면 중복되는 결과가 제거된다.

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

위 컬렉션 조인의 예제에서 TEAM과 회원 컬렉션을 조인했으므로 회원 컬렉션도 함께 조회할 것으로 기대해선 안된다.  JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다.  따라서 팀 엔티티만 조회하고 연관된 회원 컬렉션은 조회하지 않는다.  만약 회원 컬렉션을 지연 로딩으로 설정하면 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환한다.  즉시 로딩으로 설정하면 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한번 더 실행한다.

페치 조인의 특징과 한계

페치 조인을 사용하면 SQL 한번으로 연관된 엔티티들을 함게 조회할 수 잇어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다.  글로벌 로딩전략을 즉시 로딩으로 설정하면 애플리케이션 전체에 항상 즉시 로딩이 일어난다.  일부는 빠를 수 있지만 전체로 보면 사용하지 않는 엔티티를 자주 로딩하므로 오히려 성능에 악영향을 미칠 수 있다.

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

경로 표현식

경로 표현식이라는 것은 쉽게 말해서 .(점)을 찍어서 객체 그래프를 담색하는 것이다.

EX) m.team, t.name

  • 상태 필드: 단순히 값을 저장하기 위핸 필드 (m.name, m.age)
  • 연관 필드: 연관관계를 위한 필드, 임베디드 타입 포함 (m.team, team.members)

경로 표현식과 특징

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

서브 쿼리

JPQL도 SQL처럼 서브 쿼리를 지원한다.  여기에는 몇가지 제약이 있는데 서브쿼리를 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에는 사용할 수 없다.

@Test
public void test19() {
    TypedQuery<Member> query = em.createQuery("select m from Member m where m.age > (select avg(m2.age) from Member m2)", Member.class);

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

    resultList.forEach(System.out::println);
}

조건식

연산자 우선순위

  1. 경로 탐색 연산: '.'
  2. 수학 연산: +, -, *, /
  3. 비교 연산: >=, >, <=, <, <>, BETWEEN, LIKE, IN, IS NULL, IS EMPTY, MEMBER, EXIST
  4. 논리연산: NOT, AND, OR

CASE식

@Test
public void test20() {
    Query query = em.createQuery("select 
    	case t.name 
            when 'team1' then '인센티브110%' 
            when 'team2' then '인센티브120%' 
            else '인센티브105%' 
        end 
    from Team t");

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

    resultList.forEach(System.out::println);
}

Coalesce

스칼라식을 차례대로 조회해서 null이 아니면 반환한다.

@Test
public void test21() {
    Query query = em.createQuery("select coalesce(m.name, '이름없는 회원') from Member m");

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

    resultList.forEach(System.out::println);
}

기타 정리

  • enum은 = 비교연산만 지원한다
  • 임베디드 타입은 비교를 지원하지 않는다

EMPTY STRING

jpa 표준은 ''을 길이 0인 Empty String으로 정했지만 데이터베이스에 따라 ''을 null로 사용하는 데이터베이스도 있으므로 확인하고 사용

NULL 정의

  • 조건을 만족하는 데이터가 하나도 없으면 NULL이다
  • NULL은 알 수 없는 값이다. NULL과의 모든 수학적 계싼 결과는 NULL이 된다
  • NULL == NULL은 알 수 없는 값이다
  • NULL is NULL은 참이다

엔티티 직접 사용

기본 키 값

객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본 키 값으로 식별한다.

select count(m.id) from Memeber m
select count(m) from Member m

두 번째의 쿼리는 엔티티의 별칭을 직접 넘겨주었다.  이렇게 엔티티를 직접 사용하면 JPQL이 SQL로 변환될 때 해당 엔티티의 기본 키를 사용한다.

외래 키 값

@Test
public void test23() {
    Query query = em.createQuery("select m from Member m where m.team = 1L");

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

    resultList.forEach(System.out::println);
}

키본 키값이 1L인 팀 엔티티를 파라미터로 사용하고 있다.  m.team은 형재 team_id라는 외래 키와 매핑되어 있다.  쿼리를 보면 Member와 Team 간에 묵시적 조인이 일어날 것 같지만 Memeber 테이블이 team_id 외래 키를 가지고 있으므로 묵시적 조인은 일어나지 않는다. m.team.name을 사용하면 면 묵시적 조인이 일어난다.

Named 쿼리

  • 동적 쿼리: em.createQuery() 처럼 JPQL을 문자로 완성해서 직접 넌기는 것을 동적 쿼리라 한다.
  • 정적 쿼리: 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을 Named 쿼리라 한다.

Named 쿼리는 이름 그대로 쿼리에 이름을 부여해서 사용하는 방법이다.

@Test
public void test24() {
    TypedQuery<Member> query = em.createNamedQuery("Member.findByName", Member.class);

    query.setParameter("name", "name1");

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

    resultList.forEach(System.out::println);
}


 

GitHub - klyhyeon/JPAStudy

Contribute to klyhyeon/JPAStudy development by creating an account on GitHub.

github.com

 

댓글