[JPA] JPQL(1)

imcool2551·2022년 4월 10일
1

JPA

목록 보기
10/12
post-thumbnail

본 글은 인프런 김영한님의 JPA 로드맵을 기반으로 정리했습니다.

1. 데이터베이스 접근 방법


이전 글들에서 JPA를 통해 데이터베이스의 테이블을 객체지향적으로 설계한 엔티티로 매핑하는 방법을 알아보았다. 매핑을 다 마쳤다면 자바 애플리케이션에서 데이터베이스에 접근하는 방법을 알아볼 필요가 있다. 대표적으로 다음과 같은 방법들이 있다.

  • JDBC API: 순수한 자바 코드를 통해 데이터베이스 접근한다.

  • SpringJdbcTemplate: JDBC API에 템플릿 메서드 패턴을 적용하여 커넥션을 얻는 등의 반복적인 코드를 줄여준다. 여전히 SQL 코드를 작성해야한다.

  • MyBatis: 흔히 SQL 매퍼라고 불린다. 프로그램 코드와 SQL코드를 분리할 수 있다. 보통 xml파일에 SQL 코드를 작성한다. 여전히 SQL 코드를 작성해야한다.

  • JPQL: 엔티티를 통해 데이터베이스에 접근할 수 있다. SQL과 코드의 형태가 매우 유사하기 때문에 SQL에 익숙하다면 조금만 연습해도 직관적으로 사용할 수 있다. 테이블 중심이 아닌 엔티티 중심으로 개발을 할 수 있게된다.

  • JPA Criteria: 엔티티를 통해 데이터베이스에 접근할 수 있다. JPQL로 해결하기 힘든 동적 쿼리 문제를 자바 코드로 해결한다. 표준이라는 장점이 있지만 다루기 복잡하고 실용성이 떨어진다.

  • QueryDSL: 오픈 소스 라이브러리. JPQL로 해결하기 힘든 동적 쿼리 문제를 자바 코드를 통해 실용적으로 해결한다. 실무 사용을 권장한다.

이번 글에서는 JPQL을 집중적으로 알아보겠다.

2. JPQL


JPQL(Java Persistence Query Language)은 SQL을 추상화한 객체 지향 쿼리 언어다. SQL을 추상화했기 때문에 특정 데이터베이스에 의존하지 않는다. 또한 순수 JDBC, MyBatis, SpringJdbcTemplate 으로는 피할 수 없는 SQL 작성 노가다를 대푝 줄여준다. 매우 복잡한 쿼리를 작성해야 하거나 특정 데이터베이스에 의존적인 기능이 필요하다면 SQL 작성이 불가피하지만 대부분의 쿼리는 JPQL을 통해서 해결할 수 있다. JPQL의 가장 큰 장점은 데이터베이스를 테이블이 아닌 엔티티로 쿼리할 수 있다는 것이다.

JPQL도 한계가 있다. 자바 코드가 아닌 문자열로 작성되기 때문에 복잡한 동적 쿼리를 작성하기 번거롭다. 복잡한 동적 쿼리는 QueryDSL 이라는 오픈소스 라이브러리를 사용해서 해결할 수 있는데 이 또한 결국 JPQL로 변환된다. 그리고 JPQL은 데이터베이스 방언에 따라 알맞은 SQL로 변환된다.

이제부터 JPQL의 기능을 살펴보자. 사용할 엔티티 모델과 테이블 모델은 다음과 같다.

3. JPQL 기본 문법


JPQL 문법은 sql과 매우 유사하다.

# select 문
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby절]

# update 문
update_절 [where_절]

# delete 문
delete_절 [where_절]
select m from Member as m where m.age > 18

가장 기본적인 JPQL을 통해 몇 가지 규칙을 살펴보자.

  • 엔티티(Meber)와 속성(age)은 대소문자를 구별한다.

  • JPQL 키워드(select, fom, where)는 대소문자를 구별하지 않는다.

  • 별칭(m)은 필수다. as 키워드는 생략가능하다.

4. 집합과 정렬


select
  COUNT(m), // 회원수
  SUM(m.age), // 나이 합
  AVG(m.age), // 평균 나이
  MAX(m.age), // 최대 나이
  MIN(m.age) // 최소 나이
from Member m

sql에 있는 COUNT, SUM 등의 집계 함수를 JPQL에서 쓸 수 있다. 보통 집계 함수는 GROUP BY, HAVING 과 같은 집합 기능과 ORDER BY 정렬기능과 같이 쓰는 경우가 많다. 예를 들어 회원들을 연령대 별로 묶은 뒤 연봉의 평균을 구하고 연봉의 평균으로 내림차순 정렬할 수 있다.

5. TypedQuery, Query


TypedQuery는 반환 타입이 명확할 때 사용하고 Query는 반환 타입이 명확하지 않을 때 사용한다.

TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);

TypedQuery는 결과 조회시 제네릭 타입을 반환하기 때문에 사용하기 편리하다. 반환 타입이 명확하다면 TypedQuery를 사용하자.

Query query = em.createQuery("select m.username, m.age from Member m");

select 절에서 엔티티 대신 필드 두 개를 프로젝션 했기 때문에 반환 타입을 Member로 지정할 수 없다.

6. 결과 조회 API


6.1 getResultList()

String qlString = "select m from Member m";

List<Member> result = em.createQuery(qlString,Member.class)
        .getResultList();
  • 결과가 하나 이상일 때 사용. 리스트 반환

  • 결과가 없으면 빈 리스트 반환.

6.2 getSingleResult()

String qlString = "select m from Member m";

Member result = em.createQuery(qlString,Member.class)
        .getSingleResult();
  • 결과가 하나일 때 사용. 단일 객체 반환

  • 결과가 없으면 javax.persistence.NoResultException 예외 발생

  • 결과가 둘 이상이면 javax.persistence.NonUniqueResultException 예외 발생

7. 파라미터 바인딩


7.1 이름 기준

String qlString = "select m from Member m where m.username = :username";

em.createQuery(qlString, Member.class)
  .setParameter("username", "kim");

7.2 위치 기준

String qlString = "select m from Member m where m.username = ?1";

em.createQuery(qlString, Member.class)
  .setParameter(1, "kim");

위치 기준 파라미터 바인딩은 파라미터 추가시 순서가 바뀔 수 있기 때문에 이름 기준 파라미터 바인딩을 사용하자.

8. 프로젝션


프로젝션은 select 절에서 조회할 대상을 지정하는 것이다. select 절에서 sql처럼 DISTINCT 키워드를 통해 중복을 제거할 수 있다. 단, JPQL과 SQL의 DISTINCET는 약간의 차이가 있는데 이는 밑에서 알아본다.

select 절에서 조회 가능한 대상은 총 3가지다.

8.1 스칼라 타입

가장 기본적인 숫자, 문자등 기본 데이터 타입을 조회할 수 있다.

  String qlString = "select m.username from Member m"
  List<String> result = em.createQuery(qlString, String.class)
    .getResultList();

8.2 엔티티 타입

엔티티 자체를 조회할 수 있다. sql로 변환되면 member 테이블의 모든 컬럼이 select된다.

  String qlString = "select m from Member m"
  List<Member> result = em.createQuery(qlString, Member.class)
    .getResultList();

8.3 임베디드 타입

값 타입 중 하나인 임베디드 타입을 조회할 수 있다.

  String qlString = "select m.address from Member m"
  List<Address> result = em.createQuery(qlString, Address.class)
    .getResultList();

8.4 여러 값을 함께 조회하는 경우

만약 select 절에서 여러 값을 조회하면 어떤 타입으로 결과를 받을 수 있는지 살펴보자.

select m.username, m.age from Member m
  • Query 타입으로 조회

    String qlString = "select m.username, m.age from Member m"
    List result = em.createQuery(qlString)
      .getResultList();

    아무런 타입 정보를 알 수 없기 때문에 별로 도움이 되지 않는다. 게다가 Collection 타입을 제네릭 없이 쓰는 것은 좋지 않다.

  • Object[] 타입으로 조회

    String qlString = "select m.username, m.age from Member m"
    List<Object[]> result = em.createQuery(qlString)
      .getResultList();

    raw타입 컬렉션 대신 Object[] 타입이라도 주는 것이 낫다. 리스트에 레코드가 있고 컬럼들은 Object[] 배열안에 있다.

  • new 명령어로 조회

    String qlString = "select new jpql.jpql.UserDto(m.username, m.age) from Member m"
    List<MemberDto> result = em.createQuery(qlString, MemberDto.class)
      .getResultList();

    조회 결과를 DTO로 바로 받을 수 있다. DTO 클래스에 컬럼의 타입과 순서가 일치하는 생성자를 만들어야한다. DTO 클래스의 패지키명까지 모두 적어줘야해서 코드가 약간 길어지지만 여러 값을 조회하는 방법 중에는 이 방법이 가장 깔끔하다.

9. 페이징 API


JPQL은 SQL을 추상화했다고 언급했다. 그 장점이 가장 잘 드러나는 부분중 하나가 페이징 API다. 특정 데이터베이스들은 페이징 쿼리를 짜기가 매우 번거롭지만 JPQL은 페이징을 매우 쉽게 짤 수 있도록 도와준다.

단 2개의 함수만 사용하면 된다.

  • setFirstResult(int startPosition): 조회 시작 위치(0부터 시작)

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

String qlString = "select m from Member m order by m.age desc";

List<Member> result = em.createQuery(qlString, Member.class)
  .setFirstResult(10)
  .setMaxResults(10)
  .getResultList();

10. 조인


조인은 관계형 데이터베이스의 꽃이라고 할 수 있다. 데이터베이스의 조인은 3가지로 분류할 수 있으며 JPQL에서도 모두 지원한다.

10.1 내부 조인

select m from Member m [inner] join m.team t

내부 조인(inner join)이다. 키워드 inner는 보통 생략한다. Member와 Team을 조인해서 조인 결과가 있는 Member만 조회한다. m.team은 경로 표현식 문법으로 다음 글에서 살펴본다.

10.2 외부 조인

select m from Member m left [outer] join m.team t

외부 조인(outer join)이다. 키워드 outer는 보통 생략한다. Member와 Team을 조인해서 조인 결과가 없는 Member도 함께 조회한다.

10.3 세타 조인

select count(m) from Member m, Team t where m.username = t.name

from 절에 엔티티 두 개가 있기 때문에 join 키워드가 없지만 조인이 일어난다. 내부 조인이나 외부 조인과 달리 조인의 기준이 되는 키가 없기 때문에 모든 레코드를 조인하는 카티시안 곱이 생성된다. 이는 데이터가 많다면 성능상 치명적일 수 있다. 세타 조인이 필요한 경우는 거의 없다.

11. ON절


데이터베이스의 ON절을 JPQL에서도 사용할 수 있다. ON절은 크게 2가지 경우에 사용된다.

11.1 조인 대상 필터링

내부 조인과 외부 조인은 기본키와 외래키를 조인 조건으로 삼는다. ON절을 활용하여 조인 조건을 추가해서 조인해야 할 레코드가 줄임으로써 조인의 성능을 높일 수 있다.

select m, t from Member m left join m.team t on t.name = 'teamA'

Member와 Team을 조인하는데 팀의 이름이 'teamA'인 팀만 조인하도록 조인 대상을 필터링 했다. 위의 JPQL은 다음의 SQL로 번역된다.

SELECT member.*,
       team.*
FROM   member m
       LEFT JOIN team t
              ON m.team_id = t.id
                 AND t.name = 'teamA'

11.2 연관관계 없는 엔티티 외부 조인

ON절은 연관관계가 없는 엔티티를 외부 조인하는데에도 활용된다.

select m, t from Member m left join on m.username = t.name

회원의 이름과 팀의 이름이 같은 레코드들을 외부 조인했다. m.team처럼 경로표현식을 통해 연관된 엔티티를 조인하는 것이 아님에 주의하자. 번역된 sql은 다음과 같다.

SELECT m.*,
       t.*
FROM   member m
       LEFT JOIN team t
              ON m.username = t.name

12. 서브 쿼리


JPQL을 통해 서브 쿼리를 할 수 있다. 예제를 통해 서브 쿼리를 살펴보자.

  • 나이가 평균보다 많은 회원을 조회

    select m from Member m
    where m.age > (select avg(m2.age) from Member m2)

    위와 같이 외부 쿼리와 서브 쿼리에서 같은 엔티티를 사용하는 경우 alias를 분리하는 것이 성능상 나은 경우가 많다.

  • 주문 건이 한 건 이상인 회원을 조회

    select m from Member m
    where (select count(o) from Order o where m = o.member) > 0

서브 쿼리 앞에 exists, all, any(some), in 을 붙일 수 있다.

exists는 서브 쿼리의 결과가 존재하면 참을 반환한다. not exists를 통해 반환값을 뒤집을 수 있다.

all은 조건을 모두 만족하면 참을 반환하고, any(some)은 조건을 하나라도 만족하면 참을 반환한다.

in은 서브 쿼리의 결과 중 하나라도 같은 것이 존재하면 참을 반환한다. not in을 통해 반환값을 뒤집을 수 있다.

  • teamA 소속인 회원을 조회 (exists)

    select m from Member m
    where exists (select t from m.team t where t.name = 'teamA')
  • 어느 팀에라도 소속된 회원을 조회(any)

    select m from Member m
    where m.team = any(select t from Team t)

JPQL 서브 쿼리는 where, having, select 절에서 사용할 수 있지만 from 절에서 사용할 수 없다. from 절에 서브 쿼리가 필요한 경우 조인으로 풀어야한다. 대부분의 서브 쿼리는 조인으로 바꿀 수 있는 경우가 많다.

13. 조건식 - CASE, COALESCE, NULLIF


sql의 분기문이라고 할 수 있는 CASE식을 JPQL에서도 지원한다. 예제를 살펴보자.

  • 기본 CASE식

    select
      case
           when m.age <= 10 then '어린이 요금'
           when m.age >= 60 then '경로 요금'
           else '일반 요금'
      end
    from Member m

    sql처럼 case, when, then, else, end 를 조합하여 조건별로 반환값을 다르게 정할 수 있다.

  • 단순 CASE식

    select
      case t.name
           when 'teamA' then '우승팀'
           when 'teamB' then '준우승팀'
           else '일반팀'
      end
    from Team t

    마치 자바의 switch문 처럼 단순하게 값을 직접 비교하는 분기문도 가능하다.

  • COALESCE

    select coalesce(m.username,'이름 없는 회원') from Member m

    coalesce문은 값을 하나씩 조회해서 null이 아닐 때 반환한다. null대신 기본값을 지정하고 싶을 때 사용할 수 있다.

  • NULLIF

    select nullif(m.username, '관리자') from Member m

    nullif는 두 값이 같으면 null을 반환한다. 다르면 첫 번째 값을 반환한다.

14. 기본 함수


JPQL은 sql을 다뤄봤다면 익숙할법한 다양한 기본 함수들을 제공한다.

문자열을 다룰 때 사용하는 CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE 함수를 제공한다. 수학 함수인 ABS, SQRT, MOD를 제공한다. JPQL은 sql에서는 불가능한 컬렉션을 표현할 수 있기 때문에 컬렉션을 다루는 SIZE, INDEX 함수를 제공한다.

15. 이어서


이번 글에선 JPQL 개념, 기본 문법, 다양한 기능들을 살펴보았다. 다음 글에서는 경로 표현식을 알아본 뒤 연관된 엔티티를 한 번에 조회하는 페치 조인을 알아보겠다.

profile
아임쿨

0개의 댓글