Home JPA - 객체 지향 쿼리 언어(2)
Post
Cancel

JPA - 객체 지향 쿼리 언어(2)

경로 표현식

  • 상태필드(state field)
    • 경로 탐색의 끝이며 더이상 탐색 불가
  • 단일 값 연관 경로
    • 묵시적 내부 조인(inner join) 발생
    • 이후 상태필드까지 탐색 가능
  • 컬렉션 값 연관 경로
    • 묵시적 내부조인 발생
    • 더이상 탐색 불가
    • FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칠을 통해 탐색 가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 상태 필드 경로 탐색
-- JPQL
select m.username, m.age from Member m

-- SQL
select m.username, m.age from Member m

-- 단일 값 연관 경로 탐색
-- JPQL
select o.member from Order o

-- SQL
select m.*
from Order o
inner join Member m on o.member_id = m.id

명시적 조인, 묵시적 조인

  • 명시적 조인
    • join 키워드 직접 사용
    • select m from Member m join m.team t
  • 묵시적 조인
    • 경로 표현식에 의해 묵시적으로 SQL 조인 발생
    • 내부 조인만 가능
    • select m.team from Member m

경로 탐색으로 인해 발새하는 묵시적 조인은 항상 내부 조인을 수행 함. 또한 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM(JOIN) 절에 영향을 줌.

결국 경로 탐색으로 발생하는 묵시적 조인은 JPQL과 실제 변환되서 수행되는 SQL과 차이를 만들어 한눈에 쿼리를 이해하기 어렵게 만듬.

그러므로 가급적 묵시적 조인 대신 명시적 조인을 사용 해야함. 명시적 조인을 사용해야 한눈에 조인을 사용하는 것을 이해할 수 있으며, 조인은 SQL 튜닝에 중요 포인트 이므로 추후 수정에도 용이 함.

페치 조인(fetch join)

실무에서 굉장히 많이 사용되는 기능

  • SQL 조인의 종류가 아니고, JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함꼐 조회하는 기능
    • 즉, 즉시로딩을 통해 연관된 엔티티를 한번에 조회
1
2
3
4
5
6
7
-- JPQL
select m from Member m join fetch m.team

-- SQL
select m.*, t.*
from MEMBER m
inner join TEAM t on m.TEAM_ID = t.ID
  • 회원을 조회 하면서 연관된 팀도 함꼐 조회(SQL 한번에)
  • SQL을 보면 회원 뿐만 아니라 TEAM(T.*)도 함꼐 select 함

페치 조인 예시

Alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String jpql =
  "select m
   from Member m
   join fetch m.team";

List<Member> members =
  em.createQuery(jpql, Member.class).getResultList();

for (Member member : members) {
    //페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X
    System.out.println(
      "username = " + member.getUsername() + ", " +
      "teamName = " + member.getTeam().name());
}
1
2
3
username = 회원1, teamname = 팀A
username = 회원2, teamname = 팀A
username = 회원3, teamname = 팀B

컬렉션에서의 페치 조인

일대다 관계에서의 컬렉션 페치 조인

1
2
3
4
5
6
7
8
9
10
11
-- JPQL
select t
from Team t
  join fetch t.members
where t.name = 'A'

-- SQL
select t.*, m.*
from TEAM t
  inner join MEMBER m on t.ID = m.TEAM_ID
where t.NAME = 'A'

컬렉션에서의 페치 조인 예시

Alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
String jpql =
  "select t
   from Team t
    join fetch t.members
   where t.name = '팀A'";

List<Team> teams =
  em.createQuery(jpql, Team.class).getResultList();

for(Team team : teams) {
    System.out.println("teamname = " + team.getName() + ", team = " + team);

    for (Member member : team.getMembers()) {
      //페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
      System.out.println(-> username = " + member.getUsername()+ ", member = " + member);
  }
}
1
2
3
4
5
6
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300

페치조인과 DISTINCT

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령
  • JPQL의 DISTINCT는 2가지 기능 제공
    • SQL에 DISTINCT 구문 추가
    • 어플리케이션에서 엔티티 중복 제거
1
2
3
4
select distinct t
from Team t
  join fetch t.members
where t.name = ‘팀A

이와 같은 쿼리일때, SQL에 DISTINCT를 추가 한다고 해도 데이터가 다르면 SQL 결과에서 중복제거가 되지 않음

Alt text

그러므로 JPQL에서는 데이터베이스에서 이 중복된 데이터까지 통째로 가져오되, 추가로 어플리케이션 레벨에서 중복 제거를 시도 함.

Alt text

  • 같은 식별자를 가진 엔티티 제거

하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 어플리케이션에서 중복제거가 자옹으로 적용

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

일반 조인 실행시 연관된 엔티티를 함꼐 조회하지 않음

  • 일반 조인
    • JPQL은 결과를 반환할 떄 연관관계를 고려하지 않고 단지 SELECT 절에 지정한 엔티티만 조회 함
      • 위 예시에서는 팀 엔티티가 회원엔티티와 연관관계가 있으며 지연로딩으로 되어있을 떄, 팀 엔티티만 조회하고 회원 엔티티를 조회하지 않음
  • 페치 조인
    • 페이 조인을 사용할 때만 연관된 엔티티도 함꼐 조회
      • 즉, 즉시 로딩이 수행
    • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-- 일반 join 예시
-- JPQL
select t
from Team t
  join t.members m
where t.name = 'A'

-- SQL
select t.*
from TEAM t
  inner join MEMBER m on t.ID = m.TEAM_ID
where t.NAME = 'A'

-- fetch join 예시
-- JPQL
select t
from Team t
  join fetch t.members
where t.name = 'A'

-- SQL
select t.*, m.*
from TEAM t
  inner join MEMBER m on t.ID = m.TEAM_ID
where t.NAME = 'A'

페치조인의 특징과 한계

  • 한계점
    • 페치 조인 대상에는 별칭을 줄 수 없음
      • 하이버네이트에선 가능하지만 가급적이면 사용하면 안됨
      • 페치 조인은 연관된 엔티티에 대하여 모든 데이터를 가져오는 것을 기본으로 하기 떄문에 별칭을 사용하여 where 절로 조건을 걸게되면 모든 데이터를 가져오지 못하고 데이터에 대한 정합을 보장해주지 않음
    • 둘 이상의 컬렉션은 페치 조인 할수 없음
      • 일 x 다 x 다(일대다 x 다대다) 로 형성되어 조인의 결과 데이터에 빈 칼럼이 많이 생기게 될수 있어 데이터에 대한 정합을 보장할 수 없음
    • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResult)를 사용할 수 없음
      • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
      • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징을 하는데 이는 매우 위험하므로 사용하지 않는 것이 좋음
  • 특징
    • 연관된 엔티티들을 SQL 한번으로 조회
      • 성능 최적화에 이점
    • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선시 됨
      • @OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
    • 실무에서 글로벌 로딩 전략은 모두 지연 로딩을 사용하되, 최적화가 필요한 곳은 페치 조인을 사용

페치 조인 정리

  • 모든 것을 페치 조인으로 해결할 수는 없음
  • 페치 조인은 객체 그래프를 유지할 떄 사용하면 효과적
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적

다형성 쿼리

조회 대상을 특정 자식으로 한정

  • 자바의 타입 캐스팅과 유사
  • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 떄 사용
  • FROM, WHERE, SELECT(하이버네이트 지원) 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- Item 중에 Book, Movie를 조회
-- JPQL
select i
from Item i
where type(i) in (Book, Movie)

-- SQL
select i
from ITEM i
where i.DTYPE in ('B', 'M')

-- 부모인 Item중에 Book 이면서 저자가 kim 인 item 조회
-- JPQL
select i
from Item i
where treat(i as Book).author = 'kim'

-- SQL
select  i.*
from ITEM i
where i.DTYPE = 'B' and i.author = 'kim'

엔티티 직접 적용

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용 함. 만약 엔티티의 연관관계가 있는 타 엔티티를 직접 사용할 땐, 외래 키 값을 사용 함.

엔티티를 직접 사용하여 기본 키값을 사용할 떄

1
2
3
4
5
6
7
8
9
10
11
12
13
// 엔티티를 파라미터로 전달
String jpql = select m from Member m where m = :member;
List resultList =
  em.createQuery(jpql)
    .setParameter("member", member)
    .getResultList();

// 식별자를 직접 전달
String jpql = select m from Member m where m.id = :memberId;
List resultList =
  em.createQuery(jpql)
    .setParameter("memberId", memberId)
    .getResultList();
1
select m.* from Member m where m.id=?


연관관계에 있는 엔티티를 직접 사용하여 외래 키값을 사용할 떄

1
2
3
4
5
6
7
8
9
10
11
12
13
// 연관관계에 있는 엔티티를 파라미터로 전달
String jpql = select m from Member m where m.team = :team;
List resultList =
  em.createQuery(jpql)
      .setParameter("team", team)
      .getResultList();

// 식별자를 직접 전달
String jpql = select m from Member m where m.team.id = :teamId;
List resultList =
  em.createQuery(jpql)
    .setParameter("teamId", teamId)
    .getResultList();
1
select m.* from Member m where m.team_id=?

Named 쿼리

  • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
  • 정적 쿼리
  • 어노테이션, XML에 정의
    • XML이 항상 우선권을 가짐
    • 어플리케이션 운영 환경에 따라 다른 XML을 배포 할 수 있음
  • 어플리케이션 로딩 시점에 초기화 후 재사용
  • 어플리케이션 로딩 시점에 쿼리를 검증
    • 쿼리에 대한 문법에러를 어플리케이션 로딩시점(어플리케이션 시작시점)에 확인할 수 있음

어노테이션 방식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@NamedQuery(
        name = "Member.findByUsername",
        query="select m from Member m where m.username = :username")
public class Member {
    ...
}

...

List<Member> resultList =
  em.createNamedQuery("Member.findByUsername", Member.class)
    .setParameter("username", "회원1")
    .getResultList();


xml에 정의 persistence.xml 혹은 ormMember.xml 파일에 정의

1
2
3
4
<!-- META-INF/persistence.xml -->

<persistence-unit name="jpabook" >
    <mapping-file>META-INF/ormMember.xml</mapping-file>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- META-INF/ormMember.xml -->

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
    <named-query name="Member.findByUsername">
        <query><![CDATA[
            select m
            from Member m
            where m.username = :username
        ]]></query>
    </named-query>
    <named-query name="Member.count">
        <query>select count(m) from Member m</query>
    </named-query>
</entity-mappings>

벌크 연산

예를 들어 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면 아래와 같은 작업이 수행됨

  • 재고가 10개 미만인 상품을 리스트로 조회
  • 상품 엔티티의 가격을 10% 증가
  • 트랜잭션 커밋 시점에 변경감지가 동작
  • 변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행

즉, JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행되는 문제가 있음 이에 대한 문제를 JPA는 벌크 연산을 통해 한번에 여러 테이블을 업데이트 할 수 있게 함


벌크 연산

  • 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
  • executeUpdate()의 결과는 영향받은 엔티티의 수 반환
  • UPDATE, DELETE 지원
  • 하이버네이트에선 INSERT(insert into … select) 지원
1
2
3
4
5
6
7
8
9
// 재고가 10개 미만인 상품을 조호하여 상품의 가격을 10% 증가

String jpql = "update Product p " +
              "set p.price = p.price * 1.1 " +
              "where p.stockAmount < :stockAmount";

int resultCount = em.createQuery(jpql)
                    .setParameter("stockAmount", 10)
                    .executeUpdate();

벌크 연산 주의점

  • 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이트에 집접 쿼리
    • 벌크 연산으로 인하여 버그를 유발시키지 않기 위하여 아래와 같은 방법을 사용
      • 벌크 연산을 가장 먼저 실행하는 방법
      • 벌크 연산 수행 후, 영속성 컨텍스트를 초기화하여 다시 갱신하는 방법

참고

  • 자바 ORM 표준 JPA 프로그래밍(김영한)
This post is licensed under CC BY 4.0 by the author.