쌍문동 척척박사
Spring / ReactJS / PS

객체지향 쿼리 언어 JPQL

객체지향 쿼리 언어 JPQL

JPQL이란?

사용자는 JPA를 사용해 테이블이 아닌 엔티티 객체를 중심으로 개발을 한다. 하지만 문제는 검색 쿼리 시 필요한 데이터만 불러오기 위해서 검색 조건을 사용해야 하는데, 순수 JPA를 사용하면 모든 테이블 데이터를 객체로 변환한 뒤 조건에 따라 걸러내야 하는 문제점이 생긴다. 그렇지 않으면 결국 다시 SQL에 의존해야 된다.

이런 문제를 해결하기 위해 JPA는 자체적으로 SQL을 추상화한 JPQL (Java Persistence Query Language)이라는 객체 지향 쿼리 언어를 제공한다.

  • JPQL : Entity를 대상으로 쿼리
  • SQL : DB Table 대상으로 쿼리
1
2
3
4
5
// JQPL
List<User> res = em.createQuery("select u from User as u order by u.name desc", User.class)
        .setFirstResult(10)
        .setMaxResults(20)
        .getResultList();

자동 생성된 SQL문

1
2
3
4
5
6
7
8
9
SELECT
  u.id AS id,
  u.age AS age,
  u.team_id AS team_id,
  u.name AS name
FROM
  user u
ORDER BY
  u.name DESC LIMIT 10, 20
  • 엔티티와 속성은 대소문자를 구분해야 한다. (User, name)
  • JPQL 키워드는 대소문자를 구분하지 않는다. (select, SELECT)
  • 객체 지정 시 테이블 이름이 아닌 엔티티 이름을 사용한다. (User)
  • 별칭을 필수로 사용해야 한다. (User u)
  • ANSI 표준에서 지원하는 SQL 내장 함수나 집합, 정렬 등 기본적인 문법은 모두 지원된다.
    • GROUP BY, HAVING
    • ORDER BY
    • COUNT(u), SUM(u.age), MIN(u.age)
    • 서브 쿼리

페이징 API

JPA는 페이징을 API로 추상화해 제공한다.

1
2
3
4
5
// JQPL
List<User> res = em.createQuery("select u from User as u", User.class)
    .setFirstResult(10) // start position
    .setMaxResults(20)  // max data count
    .getResultList();

엔티티 직접 사용하기

1
2
3
4
5
/* JPQL */
select count(u.id) from User u;
select count(u) from User u;
/* 둘다 같은 SQL로 변환됨 */
select count(u.id) as cnt from User u;

파라미터에도 엔티티를 전달할 수 있다. 전달된 엔티티는 기본키 값 또는 외래키 값으로 변환된다.

조건식 (CASE)

1
2
3
4
5
6
select
  case  when u.age < 20 then '청소년'
        when u.age >= 60 then '노약자'
        else '성인'
  end
from User u
1
2
3
4
5
6
select
  case  when '준영' then '마피아'
        when '준혁' then '형사'
        else '민간인'
  end
from User u

경로 표현식

  • 상태 필드 : 경로 탐색의 끝. 더 이상 탐색이 불가능
  • 연관 필드 : 연관 관계를 위한 필드
    • 단일 값 연관 필드 : @ManyToOne, @OneToOne, 엔티티
    • 컬렉션 값 연관 필드 : @ManyToMany, OneToMany, 컬렉션
1
2
3
4
5
6
/* 상태 필드 */
select u.username from User u
/* 단일 값 연관 필드 */
select u.team from User u
/* 컬렉션 값 연관 필드 */
select t.users from Team t

주의할 것은 연관 필드를 사용하면 묵시적 조인이 발생해 최적화가 복잡해진다. 최대한 명시적 조인을 사용하자.

Join

@@ 기본 조인

@@ 명시적 조인, 묵시적 조인

Fetch Join

페치 조인은 연관된 엔티티나 컬렉션을 1번의 SQL로 한꺼번에 조회하는 기능으로, SQL에서 지원하는 공식 조인이 아니라 JPQL에서 성능 최적화를 위해 제공하는 기능이다.

1
2
3
4
/* JPQL */
select u from User u join fetch u.team
/* SQL */
select u.*, t.* from User u inner join Team t on u.team_id = t.id

관련 포스트 : N+1 문제와 Fetch Join

Entity Fetch Join

1
2
List<User> users = em.createQuery("select u from User u join fetch u.team", User.class)
    .getResultList();

10.png

11.png

Collection Fetch Join

1
2
List<Team> teams = em.createQuery("select t from Team t join fetch t.members where t.name = 'Team A'", Team.class)
    .getResultList();

12.png

13.png

Fetch Join의 특징

1) 페치 조인에는 별칭을 줄 수 없다.

1
2
3
select t from Team t join fetch t.members;
/* error */
select t from Team t join fetch t.members as m;

2) 2개 이상의 컬렉션은 페치 조인을 할 수 없다.

Distinct 만으로는 더 이상 중복을 제어할 수 없어, 중복 데이터 수가 예상치 못한 수로 늘어날 수 있다.

3) 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.

마찬가지로 중복 데이터 수가 예상치 못한 수로 늘어날 수 있다. 단, 1:1, N:1 같은 단일 값 연관 필드는 페치 조인해도 페이징이 가능하다.

따라서 페이징을 해야되는 경우 참조 방향 1->N에서 N->1로 뒤짚어서 해결해도 된다. (Lazy Loading. N+1 문제 발생하긴 함)

4) 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선시된다.

1
@OneToMany(fetch = FetchType.LAZY)

따라서 글로벌 로딩 전략은 모두 지연 로딩으로 지정하되, 최적화가 필요한 곳에서만 페치 조인을 적용하면 된다.

Named Query

1
2
3
4
5
6
7
@Entity
@NamedQuery(
  name = "User.findByUsername",
  query="select u from User u where u.username = :username")
public class User {
  // ...
}
1
2
3
List<User> users = em.createNamedQuery("User.findByUsername", User.class)
    .setParameter("username", "사용자1")
    .getResultList();
  • 미리 이름과 함께 정의한 JPQL
  • 정적 쿼리만 가능하다. (어플리케이션 수행 중 쿼리 형식 수정 불가)
  • 어플리케이션 로딩 시점에 SQL문으로 변환되어 캐싱되어 있어 실행 cost가 줄어든다.
  • 어플리케이션 로딩 시점에 쿼리가 검증된다.

Spring Data JPA와 같이 사용하는 경우 다음과 같은 형식으로 사용된다.

1
2
3
4
public interface UserRepository extends JpaRepository<User, Long> {
  @Query("select u from User u where u.email = ?1")
  User findByEmail(String email);
}

벌크 연산

만약 500개의 데이터를 수정하거나 삭제할 때 기존 방법대로 Dirty Checking으로 변경을 감지해 쿼리문을 날리는 경우 500번의 쿼리문이 실행되야 한다. 따라서 쿼리 한 번에 여러 테이블 로우를 변경하기 위해 벌크 연산을 지원한다.

1
2
int resultCnt = em.createQuery("update User u set u.age = u.age + 1")
    .excuteUpdate();

지원하는 쿼리는 UPDATE, DELETE이며, Hibernate에서는 INSERT도 지원한다.

주의

벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리를 실행하기에 영속성 컨텍스트에 남아있는 데이터와 불일치가 발생할 수 있다. 따라서 벌크 연산 수행 후에 영속성 컨테스트를 초기화해 불일치를 회피하자.

1
2
3
int resultCnt = em.createQuery("update User u set u.age = u.age + 1")
    .excuteUpdate();
em.clear();

JQPL의 한계

@@ QueryDSL

동적 쿼리 생성하기가 힘듬. String을 직접 다루다보니 코딩 과정에서 오류가 자주 발생한다.

  • JPA Criteria
  • QueryDSL
  • Native SQL
  • JDBC API 직접 사용, MyBatis와 함께 사용

오픈소스 라이브러리인 QueryDSL은 자바 코드를 JPQL로 변환해주는 JQPL 빌더 역할을 한다. 따라서 컴파일 시점에 문법 오류를 찾을 수 있다.

1
2
3
4
5
6
JPAFactoryQuery query = new JPAQueryFactory(em);
QUser u = QUser.user;
List<User> users = query.selectFrom(u)
        .where(u.age.gt(18))
        .orderBy(u.name.desc())
        .fetch();

참고 자료

자바 ORM 표준 JPA 프로그래밍

comments powered by Disqus