객체지향 쿼리 언어 - JPQL

10. 객체지향 쿼리 언어

10.1 객제치향 쿼리 소개 - JPQL

  • EntityManager.find() 메소드는 식별자로 엔티티 하나를 조회한다.
  • 특정 조건을 만족하는 여러 엔티티를 조회할 때 EntityManager.find() 메소드를 이용하면 모든 엔티티를 메모리에 올려두고 애플리케이션에서 검색해야한다. 비효율적인 방법이다.
  • 따라서 데이터베이스에서 SQL로 필요한 내용을 최대한 걸러서 조회해야 한다. SQL은 데이터베이 테이블을 대상으로 조회하기 때문에 엔티티 객체를 대상으로 조회하기 위해 JPQL을 사용한다.
  • JPA는 JPQL을 분석하여 적절한 SQL을 만들어 데이터베이스를 조회한다.
  • JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다. 방언을 이용하여 JPQL을 수정하지 않아도 데이터베이스를 변경할 수 있다.

10.2 JPQL

10.2.1 기본 문법과 쿼리 API

  • JPQL은 SELECT, UPDATE, DELETE 문을 사용할 수 있다.
  • INSERT 문은 없다. EntityManager.persist() 메소드를 사용하여 데이터를 추가한다.

1. SELECT 문

select m from Member as m where m.username='Hello'
  • 대소문자 구분: 엔티티와 속성은 대소문자를 구분한다. JPQL 키워드는 대소문자를 구분하지 않는다.
  • Member는 클래스이름이 아니라 엔티티 이름이다.
  • JPQL은 별칭을 필수로 사용해야 한다. as는 생략 가능하다.

2. TypeQuery vs Query

TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
List<Member> result = query.getResultList();
for (Member member : result) {
    System.out.println("member = " + member);
}
// SELECT 절의 조회 대상이 둘 이상이면 Object[]을 반환, 하나면 Object 반환
Query query = em.createQuery("select m.username, m.age from Member m");
List<Object[]> result = query.getResultList();
for (Object[] o : result) {
    System.out.println(o[0]);
    System.out.println(o[1]);
}
  • 반환 타입이 명확하면 TypedQuery 객체 사용
  • 반환 타입이 명확하지 않으면 Query 객체 사용

3. 결과 조회

  • query.getResultList(): 결과가 없으면 빈 컬렉션 반환
  • query.getSingleResult(): 결과가 정확히 하나일 때 사용, 결과가 없거나 1개 보다 많으면 예외 발생

10.2.2 파라미터 바인딩

  • 이름 기준 파라미터 바인딩 방식을 사용하는 것이 명확하다.
// 이름 기준 파라미터
List<Member> result = em.createQuery("select m from Member m where m.username = :username", Member.class)
                    .setParameter("username", "User1")
                    .getResultList();
// 위치 기준 파라미터, 위치 값은 1부터 시작
List<Member> result = em.createQuery("select m from Member m where m.username = ?1", Member.class)
                    .setParameter(1, "User1")
                    .getResultList();

10.2.3 프로젝션

  • SELECT 절에 조회할 대상을 지정하는 것을 말한다.
  • “SELECT [프로젝션 대상] FROM”
  • 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(문자, 숫자 등 기본 데이터 타입)

1. 엔티티 프로젝션

  • 영속성 컨텍스트에서 관리된다.
select m from Member m
select m.team from Member m

2. 임베디드 타입 프로젝션

  • 조회의 시작점이 될 수 없다.
  • 엔티티를 통해서 조회해야 한다.
  • 값 타입이므로 영속성 컨텍스트에서 관리되지 않는다.
List<Address> addresses1 = em.createQuery("select a from Address a", Address.class).getResultList(); // 잘못된 쿼리
List<Address> addresses2 = em.createQuery("select o.address from Order o", Address.class).getResultList(); // 정상 쿼리

3. 스칼라 타입 프로젝션

  • 문자, 숫자, 날짜와 같은 기본 데이터 타입
List<String> result = em.createQuery("select m.username from Member m", String.class).getResultList();

4. 여러 값 조회

  • 여러 값 조회시 TypeQuery 사용불가, Query를 사용해야 한다. TypeQuery vs Query 참고
List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m").getResultList();

5. NEW 명령어

  • 여러 값 조회시 Object[]보다 DTO객체를 이용한다.
  • SELECT 다음 NEW 명령어를 사용하여 DTO 객체를 바로 만들 수 있다.
  • 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
  • 순서와 타입이 일치하는 생성자가 필요하다.
public class UserDto {

    private String username;
    private int age;

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

List<UserDto> users = em.createQuery("select new jpabook.example.UserDto(m.username, m.age) from Member m", UserDto.class).getResultList();

10.2.4 페이징 API

  • 데이터베이스마다 페이징 처리 SQL 문법이 다르다. 방언을 이용하여 데이터베이스마다 다른 문법을 같은 API로 처리할 수 있다.
  • JPA는 페이징을 다음 두 API로 추상화했다.
    • setFirstResult(): 조회 시작위치, 0부터 시작한다.
    • setMaxResults(): 조회할 데이터 수
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
                    .setFirstResult(1)
                    .setMaxResults(5)
                    .getResultList();

10.2.5 집합과 정렬

1. 집합 함수

Object result = em.createQuery("select count(m), sum(m.age), avg(m.age), max(m.age), min(m.age) from Member m").getSingleResult();
Object[] o = (Object[]) result;
System.out.println(o[0]); // COUNT
System.out.println(o[1]); // SUM
System.out.println(o[2]); // AVG
System.out.println(o[3]); // MAX
System.out.println(o[4]); // MIN

2. GROUB BY, HAVING

List<Object[]> result = em.createQuery("select t.name, count(m), sum(m.age), avg(m.age), max(m.age), min(m.age) " +
                    "from Member m left join m.team t " +
                    "where m.age > 8 " +
                    "group by t.name " +
                    "having avg(m.age) > 4 ")
                    .getResultList();

GROUP BY와 HAVING절까지 기술한 SELECT구문을 해석하면 아래와 같다.

  1. from절에 기술한 테이블에서
  2. where절에 기술한 조건에 해당하는 내용들만 정리하고
  3. 정리된 내용들을 가지고 group by절에 나열된 컬럼에 대해서 그룹을 만든다.
  4. 만들어진 그룹별로 having절에 기술된 그룹함수로 계산하여 having절 조건에 맞는 그룹만 추리고
  5. 추려진 그룹들을 order by절에 잇는 조건으로 정렬시킨다.
  6. 마지막으로 select절에 있는 그룹화된 컬럼 또는 그룹함수를 화면에 출력한다.

3. 정렬

  • 아래 쿼리는 나이를 기준으로 내림차순, 나이가 같으면 이름을 기준으로 오름차순으로 정렬한다.
List<Member> result = em.createQuery("select m from Member m order by m.age DESC, m.username ASC", Member.class).getResultList();

10.2.6 JPQL 조인

1. 내부 조인

  • JPQL 내부 조인 구문과 SQL 조인 구문은 약간 다르다. 연관 필드로 조인한다.
  • select절에 있는 엔티티만 불러온다.
List<Member> result = em.createQuery("select m from Member m join m.team t where t.name = :teamName", Member.class)
                    .setParameter("teamName", "team1")
                    .getResultList();
select m.id, m.age, m.team_id, m.username 
from Member m 
inner join Team t on m.team_id=t.id 
where t.name=?
  • 조인한 두 개의 엔티티를 조회하려면 아래와 같이 사용한다. 또는 페치 조인을 사용한다.
List<Object[]> result = em.createQuery("select m, t from Member m join m.team t")
                    .getResultList();

for (Object[] o : result) {
    Member member = (Member) o[0];
    System.out.println(member.getUsername() + " " + member.getAge() + " " + member.getTeam().getName());
}

2. 외부 조인

List<Member> result = em.createQuery("select m from Member m left join m.team t", Member.class)
                    .getResultList();
select m.id, m.age, m.team_id, m.username 
from Member m 
left outer join Team t on m.team_id=t.id

3. 컬렉션 조인

  • 일대다 관계나 다대다 관계에서는 컬렉션을 사용한다.
  • 팀 엔티티의 회원 필드는 컬렉션 연관 필드를 사용한다.
List<Object[]> result = em.createQuery("select t, m from Team t left join t.result m")
                    .getResultList();

for (Object[] o : result) {
    Team team = (Team) o[0];
    Member member = (Member) o[1];
    
    System.out.println(member.getUsername() + " " + member.getAge() + " " + member.getTeam().getName());
}
select t.id, m.id, t.name, m.age, m.team_id, m.username 
from Team t 
left outer join Member m on t.id=m.team_id

4. 세타 조인

  • 관련없는 Member.username과 Team.name을 조인한다.
  • 테이블1과 테이블2를 교차 조인하면 각 테이블의 행의 수를 곱한 것과 같은 개수의 결과 행이 생긴다.
List<Member> result = em.createQuery("select m from Member m, Team t where m.username=t.name", Member.class)
                    .getResultList();
select m.id, m.age, m.team_id, m.username 
from Member m
cross join Team t where m.username=t.name

5. JOIN ON 절

  • 내부 조인의 ON 절은 WHERE 절을 사용할 때와 결과가 같다.
  • 보통 ON 절은 외부 조인에서 사용한다.
  • 외부 조인 ON 절 vs 외부 조인 WHERE 절
    • ON 절: 모든 회원을 조회하는데 팀 이름이 “team1” 인 경우에 조인한다. (left join 기준)
    • WHERE 절: 조인 이후 필터링, 따라서 “team1” 인 모든 회원 조회, 내부 조인이랑 결과가 같다.
List<Object[]> result = em.createQuery("select m, t from Member m left join m.team t on t.name = :teamName")
                    .setParameter("teamName", "team1")
                    .getResultList();
select m.id, t.id, m.age, m.team_id, m.username, t.name 
from Member m 
left outer join Team t on m.team_id=t.id and (t.name=?)

10.2.7 페치 조인

  • 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이다.

1. 엔티티 페치 조인

  • 회원과 연관된 팀을 함께 조회
  • 페치 조인은 별칭을 사용할 수 없다.(JPA 표준은 사용할 수 없고, 하이버네이트는 지원한다.)
 List<Member> result = em.createQuery("select m from Member m join fetch m.team", Member.class)
                    .getResultList();

for (Member member: result) {
    // 페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 발생안함
    System.out.println(member.getUsername() + " " + member.getAge() + " " + member.getTeam().getName());
}
select m.id, t.id, m.age, m.team_id, m.username, t.name 
from Member m 
inner join Team t on m.team_id=t.id

2. 컬렉션 페치 조인

  • 일대다 관계 컬렉션을 아래와 같이 페치조인하면, 데이터베이스에 팀은 2개지만 결과 리스트에는 10개가 조회된다.
  • 테이블 조인시 회원의 수 만큼 팀의 결과가 증가하기 때문이다.
List<Team> result = em.createQuery("select t from Team t join fetch t.members", Team.class)
                    .getResultList();

System.out.println(result.size()); // 10이 출력됨

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

    List<Member> members = team.getMembers();

    for (Member member : members) {
        System.out.println(member.getUsername() + " " + member.getAge() + " " + member.getTeam().getName());
    }
}
select t.id, m.id, t.name, m.age, m.team_id, m.username, m.team_id, m.id 
from Team t 
inner join Member m on t.id=m.team_id

3. 페치 조인과 DISTINCT

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령어이다.
  • JPQL의 DISTINCT는 SQL에 DISTINCT를 추가하고, 애플리케이션에서 한 번 더 중복을 제거한다.
  • 위의 조인 결과에서 SQL에 DISTINCT를 추가하여도 각 로우의 데이터가 다르므로 효과가 없지만, JPA가 애플리케이션에서 중복을 한 번 더 제거하기 때문에 중복이 제거된다.
List<Team> result = em.createQuery("select distinct t from Team t join fetch t.members", Team.class)
                    .getResultList();

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

  • JPQL은 SELECT절에 지정한 엔티티만 조회한다.
  • 따라서 아래 일반 조인은 SELECT절에 지정한 팀만 조회한다.
  • 연관된 엔티티를 지연 로딩으로 설정하면 프록시나 컬랙션 래퍼를 반환하고, 즉시 로딩으로 설정하면 쿼리가 한 번 더 실행된다. 따라서 한번에 조회하려면 페치 조인을 사용해야 한다.
List<Team> result = em.createQuery("select t from Team t join t.members m", Team.class)
                    .getResultList();
-- 일반 조인
select t.id, t.name 
from Team t 
inner join Member m on t.id=m.team_id
List<Team> result = em.createQuery("select t from Team t join fetch t.members", Team.class)
                    .getResultList();
select t.id, m.id, t.name, m.age, m.team_id, m.username, m.team_id, m.id 
from Team t 
inner join Member m on t.id=m.team_id

5. 페치 조인의 특징과 한계

  • 페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능 최적화를 할 수 있다.
  • 페치 조인은 글로벌 로딩 전략보다 우선한다. 지연 로딩으로 설정해도 페치 조인 사용시 즉시 로딩된다.
  • 페치 조인 대상에는 별칭을 줄 수 없다. JPA 표준에서는 지원하지 않지만 하이버네이트에서 지원한다. 하지만 추천하지 않는다.
  • 둘 이상의 컬렉션을 페치 조인 할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다. 하이버네이트에서 컬렉션 페치 조인하고 페이징 API를 사용하면 메모리에서 페이징 처리를 하므로 데이터가 많으면 위험하다.

10.2.8 경로 표현식

  • 경로 표현식이란 객체 그래프를 탐색하는 것을 말한다.
  • 아래 코드에서 m.username, m.team, m.orders, t.name이 모두 경로 표현식이다.
List result = em.createQuery("select m.username from Member m join m.team t join m.orders o where t.name = 'team1'")
                .getResultList();

1. 경로 표현식 용어 정리

  • 상태 필드: 단순 값 저장 필드
    • ex) m.username, m.age
  • 연관 필드: 연관관계를 위한 필드, 임베디드 타입 포함
    • 단일 값 연관 필드: @ManyToOne, @OneToOne
      • ex) m.team
    • 컬렉션 값 연관 필드: @OneToMany, @ManyToMany
      • ex) m.orders
@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}

2. 경로 표현식과 특징

1) 상태 필드 경로
  • 경로 탐색의 끝이다. 더이상 탐색이 불가하다.
ist<Object[]> result = em.createQuery("select m.username, m.age from Member m")
                    .getResultList();
select m.username, m.age 
from Member m
2) 단일 값 연관 경로
  • 묵시적으로 내부 조인이 일어난다. 계속 탐색할 수 있다.
  • 단일 값 연관 필드로 경로 탐색을 하면 SQL 에서 내부 조인이 일어나는데 이것을 묵시적 조인이라 한다. 묵시적 조인은 모두 내부 조인이다.
List<Team> result = em.createQuery("select m.team from Member m", Team.class)
                    .getResultList();
select t.id, t.name 
from Member m 
inner join Team t on m.team_id=t.id
  • 아래는 좀 더 복잡한 쿼리로 order.member.team, order.product 연관 관계로 인하여 3번의 조인이 발생한다.
  • order.address는 값 타입으로 order 테이블에 이미 포함되어 있어 조인이 발생하지 않는다.
List<Team> result = em.createQuery("select o.member.team from Order o where o.product.name = 'productA' and o.address.city = 'JINJU'", Team.class)
                    .getResultList();
select t.id, t.name 
from orders o
cross join Member m 
inner join Team t on m.team_id=t.id
cross join Product p 
where o.member_id=m.id and o.product_id=p.id and p.name='productA' and o.city='JINJU'
3) 컬렉션 값 연관 경로
  • 묵시적으로 내부 조인이 일어난다. 더는 탐색할 수 없다.
  • 단 FROM 절 에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있다.
  • 컬렉션 크기를 구할 수 있는 size 기능을 사용할 수 있다.
select t.members from Team t -- 성공
select t.members.username from Team t -- 실패
select m.username from Team t join t.members m --  성공
select t.members.size form Team t -- 성공

3. 경로 탐색을 사용한 묵시적 조인시 주의사항

  • 항상 내부 조인이다.
  • 컬렉션은 경로 탐색의 끝이다. 컬렉션으로 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 한다.
  • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM 절에 영향을 준다.
  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 힘들기 때문에 명시적 조인을 사용하는 것을 추천한다.

10.2.9 서브 쿼리

  • JPQL의 서브쿼리는 WHERE, HAVING 절에서만 사용할 수 있다. SELECT, FROM 절에서는 사용할 수 없다.
    • 하이버네이트의 HQL은 SELECT 절의 서브 쿼리도 허용한다.
-- 나이가 평균보다 많은 회원
select m from Member m where m.age > (select avg(m2.age) from Member m2)

-- 한 건이라도 주문한 고객
select m from Member m where (select count(o) from Order o where m = o.member) > 0

select m from Member m where m.orders.size > 0

서브 쿼리 함수

1) EXISTS
  • 서브쿼리에 결과가 존재하면 참이다.
-- TeamA 소속인 회원
select m from Member m where exists (select t from m.team t where t.name = 'TeamA')

select m from Member m join m.team t where t.name = 'TeamA'
2) ALL, ANY, SOME
  • 비교 연산자와 같이 사용한다. ex) =, >, >=, <, <= <>
  • ALL: 조건을 모두 만족하면 참이다.
  • ANY, SOME: 조건을 하나라도 만족하면 참이다.
-- 전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o where o.orderAmount > all (select p.stockAmount from Product p)

-- 어떤 팀이든 팀에 소속된 회원
select m from Member m where m.team = any (select t from Team t)

select m from Member m join m.team t
3) IN
  • 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참이다. IN은 서브쿼리가 아닌 곳에서도 사용한다.
-- 20세 이상을 보유한 팀
select t from Team t where t in (select t2 from Team t2 join t2.members m2 where m2.age >= 20)

10.2.10 조건식

1. 타입 표현

  • 문자: ‘Hello’, ‘She’’s’
  • 숫자: 10L, 10D, 10F
  • 날짜: {d’2012-03-24’}, {t’10-11-11}, {ts’2012-03-24 10-11-11.123’}
  • Boolean: TRUE, FALSE
  • Enum: jpabook.MemberType.Admin
  • 엔티티 타입: TYPE(m) = Member

2. 연산자 우선 순위

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

3. 논리 연산과 비교식

  • 논리 연산: AND, OR, NOT
  • 비교식: =, >, >=, <, <=, <>

4. Between, IN, Like, NULL 비교

1) Beetween 식
  • X BETWEEN A AND B: X는 A~B 사이의 값이면 참(A, B 값 포함)
select m from Member m where m.age between 10 and 20
2) IN 식
  • X IN (예제): X와 같은 값이 예제에 하나라도 있으면 참이다.
select m from Member m where m.username in ('회원1', '회원2')
3) Like 식
  • LIKE 패턴 값 [ESCAPE 이스케이프 문자]
  • %: 아무 값들이 입력되어도 된다(값이 없어도 됨).
  • _: 한 글자는 아무 값이 입력되어도 되지만 값이 있어야 한다.
select m from Member m where m.username like '%원%' -- 좋은회원, 회원, 원

select m from Member m where m.username like '회원%' -- 회원1, 회원ABC

select m from Member m where m.username like '%회원' -- 좋은회원, A회원

select m from Member m where m.username like '회원_' -- 회원A, 회원1

select m from Member m where m.username like '__3' -- 회원3

select m from Member m where m.username like '회원\%' ESCAPE '\' -- 회원%
5) NULL 비교식
  • IS NULL
  • NULL은 = 으로 비교하면 안되고 꼭 IS NULL을 사용해야 한다.
where m.username is null
where null = null -- 거짓
where 1=1 -- 참

5. 컬렉션 식

1) 빈 컬렉션 비교 식
  • IS EMPTY
select m from Member m where m.orders is not empty
2) 컬렉션의 멤버 식
  • MEMBER OF
select t from Team t where :memberParam member of t.members

6. 스칼라 식

1) 수학 식
  • +, -: 단항 연산자
  • *, /, +, -: 사칙연산
2) 문자함수
  • CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE
3) 수학함수
  • ABS, SQRT, MOD, SIZE, INDEX
  • SIZE: 컬렉션 크기
  • INDEX: List 타입 컬렉션의 위치 값을 구함, 단 컬렉션이 @OrderColumn을 사용하는 List 타입일 때만 사용할 수 있다.
4) 날짜함수
  • CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP
  • 하이버네이트는 날짜 타입에서 년, 월, 일, 시간, 분, 초 값을 구하는 기능을 지원한다.
select CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP from Team t

-- 하이버네이트 지원 기능
select year(CURRENT_TIMESTAMP), month(CURRENT_TIMESTAMP), day(CURRENT_TIMESTAMP) from Member

7. CASE 식

1) 기본 CASE
select
  case when m.age <= 10 then '학생요금'
       when m.age >= 60 then '경로요금'
       else '일반요금'
  end
from Member m
2) 심플 CASE
select
  case t.name
    when '팀A' then '인센티브110%'
    when '팀A' then '인센티브110%'
    else '인센티브105%'
  end
from Team t
3) COALESCE
  • 스칼라식을 차례대로 조회해서 null이 아니면 반환한다.
select coalesce(m.username, '이름 없는 회원') from Member m
4) NULLIF
  • 두 값이 같으면 null을 반환하고 다르면 첫 번째 값을 반환한다.
select nullif(m.username, '관리자') from Member m

10.2.11 다형성 쿼리

  • JPQL로 부모 엔티티를 조회하면 자식 엔티티도 함께 조회한다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {

    @GeneratedValue @Id
    private Long id;

    private String name;
    private int price;
}
@Entity
@DiscriminatorValue("Book")
@Getter @Setter
public class Book extends Item {

    private String author;
}
-- SINGLE TABLE 전략(InheritanceType.SINGLE_TABLE)
select i.id, i.name, i.price, i.author, i.dtype
from Item i

-- JOIN 전략(InheritanceType.JOINED)
select i.id, i.name, i.price, b.author, i.dtype 
from Item i 
left outer join Book b on i.id=b.id

1. TYPE

  • 엔티티 상속구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용한다.
List<Item> items = em.createQuery("select i from Item i where type(i) in Book", Item.class).getResultList();
-- SINGLE TABLE 전략(InheritanceType.SINGLE_TABLE)
select i.id, i.name, i.price, i.author, i.dtype 
from Item i 
where i.dtype in ('Book')
-- JOIN 전략(InheritanceType.JOINED)
select i.id, i.name, i.price, b.author, i.dtype 
from Item i 
left outer join Book b on i.id=b.id 
where i.dtype in ('Book')

2. TREAT

  • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.
  • JPA 표준은 FROM, WHERE 절에서 사용할 수 있지만 하이버네이트는 SELECT 절에서도 사용할 수 있다.
List<Item> items = em.createQuery("select i from Item i where treat(i as Book).author = 'kim'", Item.class).getResultList();
-- SINGLE TABLE 전략(InheritanceType.SINGLE_TABLE)
select i.id, i.name, i.price, i.author, i.dtype 
from Item i 
where i.dtype='Book' and i.author='kim'
-- JOIN 전략(InheritanceType.JOINED)
select i.id, i.name, i.price, b.author, i.dtype 
from Item i 
inner join Book b on i.id=b.id 
where b.author='kim'

10.2.12 사용자 정의 함수 호출

  • 사용자 정의 함수를 등록하고 사용하는 기능이다.
  • 방언 클래스를 상속하여 구현한다.

10.2.13 기타 정리

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

1. EMPTY STRING

  • JPA 표준은 ‘‘을 길이 0인 Empty String으로 정했지만 데이터베이스에 따라 ‘‘를 NULL로 사용하는 데이터베이스도 있으므로 확인해야한다.

2. NULL 정의

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

10.2.14 엔티티 직접 사용

1. 기본 키 값

  • JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값으로 변환된다.
Member result = em.createQuery("select m from Member m where m = :member", Member.class)
                            .setParameter("member", member)
                            .getSingleResult();
select m.id, m.age, m.team_id, m.username 
from Member m 
where m.id=?

2. 외래 키 값

  • JPQL에서 m.team은 SQL에서 team_id 외래 키로 변환된다.
  • JPQL에서 m.team을 연관 경로 표현식을 사용하여 묵시적 조인이 일어날 것 같지만 Member 테이블이 team_id 외래 키를 가지고 있으므로 묵시적 조인은 일어나지 않는다.
List<Member> result = em.createQuery("select m from Member m where m.team = :team", Member.class)
                    .setParameter("team", team1)
                    .getResultList();
select m.id, m.age, m.team_id, m.username 
from Member m 
where m.team_id=?

10.2.15 Named 쿼리: 정적 쿼리

  • JPQL 쿼리는 동적 쿼리와 정적 쿼리로 나눌 수 있다.
  • 동적 쿼리: em.createQuery(…) 처럼 런타임에 동적으로 JPQL을 구성하여 넘기는 것을 말한다.
  • 정적 쿼리: 미리 정의한 쿼리에 이름을 부여해서 사용하는 것을 말한다. Named 쿼리라 한다.
  • Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해둔다.
  • 따라서 오류를 빨리 확인할 수 있고, 파싱된 결과를 재사용하므로 성능상 이점이 있다.
@Entity
@NamedQuery(name = "Member.findByUsername",
            query = "select m from Member m where m.username = :username")
@Getter @Setter
public class Member {

}
List<Member> members = em.createNamedQuery("Member.findByUsername", Member.class)
                    .setParameter("username", "member1")
                    .getResultList();

Tags:

Categories:

Updated:

Leave a comment