Home 13. JPQL
Post
Cancel

13. JPQL

01. JPQL

JPQL은 JPA에서 사용하는 쿼리 언어이다. SQL의 쿼리 언어와 매우 유사하나 테이블과 칼럼 이름 대신 매핑한 엔티티 이름과 속성 이름을 사용한다는 점이 차이가 있다.

1
2
3
4
5
//엔티티 이름과 엔티티 속성을 사용한다.
select
from Review r
where r.hotel =:hotel
order by r.id desc

02. JPQL 기본 코드

select 별칭 from 엔티티이름 as 별칭 엔티티 이름 뒤에는 별칭이 오는데 이 별칭은 엔티티를 참조할 때 사용한다. 별칭은 필수이며 as는 생략할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
TypedQuery<User> query = em.createQuery("select u from User u", User.class);
List<User> users = query.getResultList(); // SQL을 실행하고 그 결과를 리턴
for(User u : users) {
  //...
}

//TypedQuery와 달리 타입을 지정하지 않는다.
Query query = em.createQuery("select u from User u");
List users = query.getResultList();
for(Object o : users) {
  User user = (User)o;
}

2.1 order by를 이용한 정렬

1
2
3
select p from Player p order by p.name asc
select p from Player p order by p.name desc
select p from Player p order by p.team.id, p.name

03. 검색 조건 지정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TypedQuery<Player> query = em.createQuery(
  "select p from Player p where p.team.id = "T1", Player.class);
List<Player> players = query.getResultList();

//인텍스 기반 파라미터를 이용해 입력 파라미터를 지정
TypedQuery<Player> query2 = em.createQuery(
  "select p from Player p where p.team.id = ? and p.salary = ? , Player.class);
query2.setParameter(0, "T1");
query2.setParameter(1, 1000);
List<Player> players2 = query2.getResultList();

//파라미터 값으로 엔티티 지정
//엔티티를 이용하면 연관에 대한 직접 비교를 할 수 있다.
Team team = em.find(Team.class, "T1");
TypedQuery<Player> query3 = em.createQuery(
  "select p from Player p where p.team.id = :team and p.salary > :minSalary , Player.class);
query3.setParameter("team", team);
query3.setParameter("minSalary", 1000);
List<Player> players3 = query3.getResultList();

3-1. 비교 연산자

  • = :값이 같은지 비교
  • <> : 값이 다른지 비교
  • <, >,>=,<,<= : 값의 크기 비교
  • between : 값이 사이에 포함되는지 비교
  • in, not in : 지정한 목록에 값이 존재하는지 또는 존재하지 않는지 비교
  • like, not like : 지정한 문자열을 포함하는지 검사
  • is null, is not null : 값이 null인지 또는 null이 아닌지

3-2. 콜렉션 비교

특정 엔티티나 값이 콜렉션에 속해있는지 검사하고 싶다면 member of 연산자나 not member of 연산자를 사용하면 된다.

1
2
3
4
5
Player player = em.find(Player.class, "P1");
TypedQuery<Team> query = em.createQuery(
  "select t from Team t where :player member of t.players order by t.name", Team.class);
query.setParameter("player", player);
List<Team> teams = query.getResultList();
1
2
3
4
5
# 실제 query 실행
select t.id, t.name
from Team
where ? in (select p.player_id from Player p where t.id = p.team.id)
order by t.name

is empty, is not empty를 사용하면 엔티티 콜렉션에 대해 콜렉션이 비어있는지 비교할 수 있다.

3-3. exists, all, any

특정 값이 존재하는지 검사하고 싶을 때 exists, all, any 중 하나를 사용한다.

  • exists : 특정 값이 존재 할 때
  • not exists : 특정 값이 존재하지 않을 때
  • all : 서브 쿼리 결과가 조건을 모두 충족하는지 검사
  • any : 서브 쿼리 결과가 조건을 충족하는 대상중 하나 이상 충족하는지 검사

04. 페이징 처리

Query, TypedQuery를 사용하면 간단하게 페이징 처리를 할 수 있다.

1
2
3
4
5
TypedQuery<Review> query = em.createQuery("select r from Review r where r.hotel.id = :hotelId order by r.id desc", Review.class);
query.setParameter("hotelId", "H-001");
query.setFirstResult(10);   //조회할 첫 번째 결과의 위치 지정
query.setMaxResults(5);     //조회할 최대 갯수
List<Review> reivews = query.getResultLiat();

05. 지정 속성 조회

5-1. 배열로 조회하기

1
2
3
4
5
6
7
8
TypedQuery<Object[]> query = em.createQuery("select p.id, p.name, p.salary from Player p", Object[].class);
List<Object[]> rows = query.getResultList();
for(Object[] row: rows) {
  String id = (String)row[0];
  String name = (String)row[1];
  int salary = (int)row[2];
  //...
}

select 절에서 선택한 대상이 두 개 이상일 때 결과 타입은 Object 배열이다. Object 배열은 순서대로 값을 보관하기 때문에 배열의 각 데이터를 사용할 때는 위 코드처럼 해당 타입으로 알맞게 변환해주어야 한다.

5-2. 특정 객체로 조회하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class IdName{
  private String id;
  private String name;

  public IdName(String id, String name) {
    this.id = id;
    this.name = name;
  }

  public String getId() {
    return id;
  }

  public String getName() {
    return name;
  }
}
1
2
3
//생성자를 이용해서 결과 객체를 생성한다.
TypedQuery<IdName> query = em.createQuery("select new com.common.IdName(p.id, p.name) from Player p", IdName.class);
List<IdName> rows = query.getResultList();

06. 한 개 행 조회

결과가 여러개인 경우 getResultList() 메서드를 이용했다. 결과가 정확하게 한 행인 경우에는 getResultList() 대신에 getSingleResult() 메서드를 사용할 수 있다.

1
2
TypedQuery<Long> query = em.createQuery("select count(p) from Player p", Long.class);
Long count = query.getSingleResult();

getSingleResult() 사용시 주의할 점은 결과가 반드시 1개여야 한다는 것이다. 결과가 없거나 두 개 이상일 경우 exception이 발생한다.

07. 조인

JPQL에서는 다음의 세 가지 방식으로 조인을 수행할 수 있다.

  • 자동 조인
  • 명시적 조인
  • where 절에서 조인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1. 자동 조인 : Player와 Team은 서로 다른 테이블에 매핑되어 있어 하이버네이트는 두 테이블을 조인한 쿼리를 실행한다.
select p
from Player p
where p.team.name = :teamName

//2. 명시적 조인
select p
from Player p
join p.team t
where t.name = :teamName

//3. where 절에서 조인
select u, s
from User u, UserBestSight s
where u.email = s.email
order by u.name

08. 집합 함수

함수리턴타입설명
countlong갯수
max, min해당 속성 타입최대값/최소값
avgDouble평균
sum속성타입에 따라 다름합계

09. group by 와 having

SQL의 group by와 동일하게 JPQL의 group by는 지정항 속성을 기준으로 그룹을 나눈다. group by에 having을 사용해서 조건을 지정할 수 있다.

1
2
3
4
5
//소속 선수가 1명보다 많은 Team의 선수 수와 평균 연봉을 구하는 JPQL
select t, count(p), avg(p.salary)
from Team t left join t.players p
group by t
having count(p) > 1

10. 함수와 연산자

10.1 문자열 함수

  • CONCAT - 두 개 이상의 문자열을 연결
  • SUBSTRING - 문자열에서 시작 위치부터 지정한 길의에 해당하는 문자열을 구함
  • TRIM - 문자열의 공백을 제거
  • LOWER - 문자열을 소문자로 변환
  • UPPER - 문자열을 대문자로 변환
  • LENGTH - 문자열의 길이를 구함
  • LOCATE - 식1 문자열에 식2가 포함된 위치를 구함

10.2 수학 함수와 연산자

  • ABS - 절대값
  • SQRT - 제곱근
  • MOD - 수식1을 수식2로 나눈 나머지
  • 그외 - 덧셈, 뺄셈, 곱셈, 나눗셈 등

10.3 날짜 시간 함수

  • CURRENT_DATE - 현재 시간을 DATE 타입으로 구함
  • CURRENT_TIME - 현재 시간을 TIME 타입으로 구함
  • CURRENT_TIMESTAMP - 현재 시간을 TIMESTAMP 타입으로 구함

10.4 콜렉션 관련 함수

  • SIZE - 콜렉션의 크기를 구함
  • INDEX - 해당 리스트의 인덱스 값 비교시 사용

11. 네임드 쿼리

JPQL이 길어지면 문저열을 위한 큰따옴표와 문자열 연결을 위한 + 연산자 때문에 코드가 복잡해진다. 이럴때 필요한 것이 네임드 쿼리로 XML 파일에 네임드 쿼리를 등록하여 사용한다.

1
2
3
4
5
6
7
8
9
<!-- .......  -->

<named-query name="Hotel.noReview">
    <query>
      select h from Hotel h
      where not exists (select r from Review r where r.hotel = h)
      order by h.name
    </query>
</named-query>

xml 파일로 네임드 쿼리를 등록했다면 persistence.xml 파일에 해당 파일을 등록해야 한다.

1
2
3
Player player = em.find(Player.class, "P1");
TypedQuery<Hotel> query = em.createNamedQuery("Hotel.noReview, Hotel.class);
//...

네임드 쿼리를 작성하는 또 다른 방법으로는 @NamedQuery 애노테이션을 이용하는 것이다. 이 애너테이션은 엔티티에 설정할 수 있으며 @Embeddedable과 같이 엔티티 이외의 타입에는 설정할 수 없다.

1
2
3
4
5
6
7
8
@Entity
@NamedQueries({
  @NamedQuery(name="Hotel.all", query="select h from Hotel h"),
  @NamedQuery(name="Hotel.findById", query="select h from Hotel h where h.id=:id)
})
public class Hotel {
    //....
}

12. N+1 쿼리와 조회 전략

N+1 쿼리는 연관이나 콜렉션과 관련되어 있다.

1
2
3
4
5
6
7
8
9
10
11
@Entity
@Table(name="membership_card")
public class MembershipCard {
   @Id
   @Column(name="card_number")
   private String number;

   @OneToOne
   @JoinColumn(name="user_email")
   private User owner;
}

@OneToOne 애노테이션의 fetch 속성의 기본값은 EAGER이다. 따라서 MembershipCard를 검색하면 조인 쿼리를 이용해서 연관된 엔티티의 데이터도 함께 조회한다.

하지만 JQPL에서는 즉시 로딩 설정을 해도 조인을 사용하지 않는다.

1
2
3
TypedQuery<MembershipCard> query = em.createQuery(
   "select mc from MembershipCard mc", MembershipCard.class);
List<MembershipCard> cards = query.getResultList();

query.getResultList()를 실행하면 MembershipCard 엔티티를 조회하기 위한 select 쿼리를 실행한 뒤 User 엔티티 로딩을 위한 쿼리를 N번 실행한다. 이유는 User와 연관을 갖는 MembershipCard 엔티티가 N개이기 때문이다. 그리고 getResultList() 메서드 실행시에도 User 엔티티 쿼리를 실행하는데 즉시로딩으로 설정되어있기 때문이다.

이렇게 N개의 연관된 객체를 로딩하기 위한 N번의 쿼리를 더 실행하는 것을 N+1 쿼리 문제라고 한다.

12-1 1:1, N:1 연관에 대한 fetch 조인

N+1 쿼리 문제를 처리하는 가장 쉬운 방법은 JPQL에서 fetch 조인을 사용하는 것이다.

1
2
3
4
5
//join 뒤에 fetch 키워드를 사용하면 조인한 대상을 함께 로딩해서 생성한다.(즉 N번 실행하지 않는다.)
TypedQuery<MembershipCard> query = em.createQuery(
   "select mc from MembershipCard me left join fetch mc.owner u", MembershipCard.class);
List<MembershipCard> cards = query.getResultList();

fetch를 사용하지 않고 join만 사용하는 경우에도 조인 쿼리를 사용하지만 연관된 엔티티를 생성하지는 않는다. 그렇게 되면 연관된 데이터를 함께 로딩했음에도 연관 엔티티를 로딩하기 위한 쿼리를 추가로 실행한다.

12-2 콜렉션 연관에 대한 fetch 조인

1
2
3
TypedQuery<Team> query = em.createQuery(
   "select t from Team t join fetch t.players p", Team.class);
List<Team> teams = query.getResultList();
1
2
3
4
# 실행되는 쿼리는 다음과 같다.
select t.id, p.player_id, t.name, p.name, p.salary, p.team_id
from Team t
inner join Player p on t.id = p.team_id

위 그림과 같은 결과가 나온다고 했을 때 쿼리 결과의 행 수는 5개이다. 그런데 JPQL에서 select 로 조회한 대상은 Team이므로 getResultList()로 구한 List는 5개의 Team 객체를 리턴한다.

1
2
3
4
5
6
7
8
TypedQuery<Team> query = em.createQuery(
   "select t from Team t join fetch t.players p", Team.class);
List<Team> teams = query.getResultList();

Team tema1 = teams.get(0);
Team team1b = teams.get(1);

Set<Player> players = team1.getPlayers();  //fetch 조인으로 이미 로딩

fetcj 조인으로 연관된 players 콜렉션을 로딩했으므로 players에 대한 연관 설정이 지연 로딩이여도 team1.getPlayers()를 처음 실행하는 시점에 Player를 로딩하기 위한 쿼리를 실행하지 않는다.

1
2
3
//인덱스가 1인 결과로부터 1개의 결과로 한정
query.setFirstResult(1);
query.setMaxResults(1);

setFirstResult나 setMaxResults를 설정하면 메모리에서 연산을 처리한다. 따라서 결과가 인덱스의 1번인 [T1]이 나와야할 것 같지만 [T2]가 나온다.

콜렉션에 대해 fetch 조인을 사용하게 되면 일단 쿼리를 실행해서 엔티티를 메모리에 모두 로딩한다. 그런 뒤 중복을 제거한다. 즉 [T1, T1, T2, T2, T2]를 메모리에 로딩한 뒤에 같은 식별자를 갖는 엔티티의 중복을 제거해서 [T1, T2]로 만든다.

이들을 함께 사용하게 되면 쿼리에 해당하는 데이터를 모두 로딩해서 메모리에서 페이징 처리를 하기 때문에 대량 데이터에는 사용하면 안된다. (OutOfMemory가 발생할 수 있음)

This post is licensed under CC BY 4.0 by the author.