본문 바로가기
Spring JPA

객체지향 쿼리 언어 - 13

by 홍굴이 2021. 7. 17.

JPQL - Java Persistence Query Language

  • 엔티티 객체를 조회하는 객체지향 쿼리 -> 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
  • SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다.
  • SQL보다 간결하다.
  • JPQL은 결국 SQL로 변경된다.
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = entityManager.createQuery(jpql, Member.class).getResultList();

SELECT 문

select m from Member as m where m.username = 'Kim'
  • 대소문자 구분
    • 엔티티와 속성은 대소문자를 구분한다.
    • select, from, as 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
  • 엔티티 이름
    • JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다.
      즉, 엔티티명은 @Entity(name = "OOO")로 지정할 수 있다.
  • 별칭은 필수
    • Member as m 을 보면 m 이라는 별칭을 주었다. 즉, JPQL은 별칭을 필수로 사용해야 한다.

TypeQuery, Query

  • 반환할 타입을 명확하게 지정할 수 있는 쿼리 객체는 TypeQuery 객체를 사용한다.
  • 반환 타입을 명확하게 지정할 수 없으면 Query 객체를 사용한다.

TypeQuery

String jpql = "select m from Member as m where m.username = 'kim'";
TypedQuery<Member> query = entityManager.createQuery(jpql, Member.class);

List<Member> resultList = query.getResultList();

Query

String jpql = "select m.username, m.age from Member as m";
Query query = entityManager.createQuery(jpql);

List resultList = query.getResultList();
for(Object object : resultList) {
    Object[] result = (Object[]) object;
    System.out.println("username : " + result[0]);
    System.out.println("age : " + result[1]);
}

결과 조회

  • query.getResultList() : 결과를 컬렉션으로 반환한다. 없으면 빈 컬렉션을 반환한다.
  • query.getSingleResult() : 결과가 정확히 하나일 때 사용한다.

파라미터 바인딩

  • 이름 기준 파라미터 -> 이름 앞에 : 을 사용한다.
    String usernameParameter = "hong";
    
    String jpql = "select m from Member as m where m.username = :username";
    TypedQuery<Member> query = entityManager.createQuery(jpql, Member.class);
    
    query.setParameter("username", usernameParameter);
    List<Member> resultList = query.getResultList();
  • 위치 기준 파라미터 -> ? 다음에 위치 값을 주면 된다.
    String usernameParameter = "hong";
            
    String jpql = "select m from Member as m where m.username = ?1";
    TypedQuery<Member> query = entityManager.createQuery(jpql, Member.class)
    		.setParameter(1, usernameParameter);
            
    List<Member> resultList = query.getResultList();​

프로젝션

1. 엔티티 프로젝션

select m from Member m				//회원 조회
select m.team from Member m			//회원의 팀 조회​

이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

 

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

String query = "select m.address from Member m";
List<Address> addressList = entityManager.createQuery(query, Address.class).getResultList();

엔티티를 시작으로 임베디드 타입을 조회한다.

임베디드 타입은 엔티티 타입이 아닌 값 타입이므로 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.

 

3. 스칼라 타입 프로젝션

String query = "select m.username from Member m";
List<String> usernameList = entityManager.createQuery(query, String.class).getResultList();

숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다.

 

4. 여러 값 조회

String jpql = "select m.username, m.age from Member as m";
Query query = entityManager.createQuery(jpql);

List resultList = query.getResultList();
Iterator iterator = resultList.iterator();

while(iterator.hasNext()) {
    Object[] row = (Object[]) iterator.next();
    String username = (String) row[0];
    String age = (Integer) row[1];
}

이렇게 조회한 엔티티도 영속성 컨테스트에서 관리된다.

 

5. New 명령어

String jpql = "select m.username, m.age from Member as m";
Query query = entityManager.createQuery(jpql);

List<Object[]> resultList = query.getResultList();

List<UserDto> userDto = new ArrayList<>();

for(Object[] object : resultList) {
    userDto.add(new UserDto((String) object[0], (Integer) object[1]);
}

이렇게 객체를 변환하는 작업 대신에 NEW 명령어를 통해 간단하게 변환할 수 있다.

String jpql = "select new package.UserDto(m.username, m.age) from Member as m";
TypedQuery query = entityManager.createQuery(jpql, UserDto.class);

List<UserDto> resultList = query.getResultList();
  • 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
  • 순서와 타입이 일치하는 생성자가 필요하다.

JPQL 페치 조인

페치(fetch) 조인은 SQL에서 조인의 종류가 아니라 JPQL에서 성능 최적화를 위해 제공하는 기능이다.

이것은 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이고 join fetch 명령어로 사용할 수 있다.

String jpql = "select m from Member m join fetch m.team";
TypedQuery<Member> query = entityManager.createQuery(jpql, Member.class);

List<Member> resultList = query.getResultList();

for(Member member : memberList) {
    System.out.println(member.getUsername() + ", " + member.getTeam().name());
}

지연로딩으로 설정했다고 가정하고 위의 코드를 실행시켜도 페치 조인을 통해 팀도 함께 조회가 되었으므로 연관된 팀 엔티티는 하이버네이트 프록시가 아닌 실제 엔티티가 나온다.

 

컬렉션 페치 조인

select t
from Team t join fetch t.memberList
where t.name = '팀A'

컬렉션 패치 조인 시도

TEAM 테이블에서 '팀A'는 하나지만 MEMBER 테이블과 조인하면서 결과가 증가해서 '팀A'가 각각 2건이 되었다.

String jpql = "select t from Team t join fetch t.memberList where t.name = '팀A'";
TypedQuery query = entityManager.createQuery(jpql, Team.class);

List<Team> resultList = query.getResultList();

for(Team team : resultList) {
    System.out.println(team.getName() + " " + team);
    
    for(Member member : team.getMemberList()) {
    	System.out.println(member.getUsername() + " " + member);
    }
}

위의 코드로 조회를 하면

팀A Team@0x100

회원1 Member@0x200

회원2 Member@0x300

팀A Team@0x100

회원1 Member@0x200

회원2 Member@0x300

'팀A'가 2건 조회된 것을 확인할 수 있다.

 

이럴경우 distinct 키워드를 사용하여 중복을 제거할 수 있다.

select distinct t
from Team t join fetch t.memberList
where t.name = '팀A'

 

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

 

일반조인

select t
from Team t join t.memberList 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'

JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다.

단지 select 절에 지정한 엔티티만 조회할 뿐이다.

따라서 지연로딩으로 설정하면 멤버에는 하이버네이트 프록시를 반환한다.

즉시로딩으로 설정하면 즉시 로딩을 위해 쿼리를 한 번 더 실행한다.

 

페치 조인의 특징과 한계

 

특징

페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다.

 

한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
  • 둘 이상의 컬렉션을 페치할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
    - 하이버네이트에서 컬렉션을 페치 조인하고 페이징 API를 사용하면 경고 로그를 남기면서 메모리에서 페이징 처리를 한다. 데이터가 많으면 성능 이슈와 메모리 초과 예외가 발생할 수 있어 위험하다.

경로 표현식

  • 상태필드  : 단순히 값을 저장하기 위한 필드
  • 연관필드
    - 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티
    - 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션

특징

  • 상태 필드 경로 : 경로 탐색의 끝
  • 단일 값 연관 경로 : 묵시적으로 내부 조인이 일어난다. 계속 탐색할 수 있다.
  • 컬렉션 값 연관 경로 : 묵시적으로 내부 조인이 일어난다. 더는 탐색할 수 없다. 단 FROM 절에서 조인을 통해 별칭을 얻으면 탐색할 수 있다. 

1. 상태 필드 경로 탐색

JPQL

select m.username, m.age from Member m

SQL

select m.name, m.age
from Member m

2. 단일 값 연관 경로 탐색

JPQL

select o.member from Order o

SQL

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

단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이것을 묵시적 조인이라고 한다.

묵시적 조인은 모두 내부 조인이다.

 

3. 컬렉션 값 연관 경로 탐색

select t.memberList from Team t			//성공
select t.memberList.username from Team t	//실패

컬렉션까지는 탐색이 가능하지만 컬렉션에서 경로 탐색을 시작하는 것은 허락하지 않는다.

 

주의사항

  • 항상 내부 조인이다.
  • 컬렉션은 경로 탐색의 끝이다.
    컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 한다.
  • 경로 탐색은 주로 select, where 절에서 사용하지만 뭇기적 조인으로 인해 SQL의 from 절에 영향을 준다.

서브 쿼리

JPQL에서 서브 쿼리는 where, having 절에서만 사용이 가능하다.

select, from 절에서는 사용이 불가능하다.


다형성 쿼리

JPQL로 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회한다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "TYPE")
public abstract class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String price;
}
@Entity
@DiscriminatorValue("movie")
public class Movie extends Item {

    private String actor;
    private String director;

}

//Album, Book 생략...
select i from Item i

다음과 같이 JPQL을 작성하여 Item을 조회할 때 단일 테이블 전략(InheritanceType.SINGLE_TABLE)을 사용하면 SQL은 다음과 같다.

select * from item

 

조인 전략(InheritanceType.JOINED)을 사용할 때 실행되는 SQL은 다음과 같다.

select
    i.item_id, i.dtype, i.name, i.price,
    b.author, b.isbn,
    a.artist, a.etcm,
    m.actor, m.director
from
	Item i
left outer join
	Book b on i.item_id = b.item_id
left outer join
	Album a on i.item_id = a.item_id
left outer join
	Movie m on i.item_id = m.item_id

TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용한다.

JPQL

select i from Item i
where type(i) in (Book, Movie)

SQL

select i from Item i
where i.dtype in ('B', 'M')

 

TREAT

JPA 2.1에 추가된 기능으로 자바의 타입 캐스팅과 비슷하다.

상속구조에서 부모 타입을 특정 자식 타입으로 바꿀 때 사용한다.

JPA 표준은 FROM, WHERE절에서 사용할 수 있지만, Hibernate는 SELECT 절에서도 사용할 수 있다.

 

JPQL

select i from Item i where treat(i as Book).author = 'Hong'

SQL

select i.* from Item i
where
    i.dtype = 'Book'
    and i.author = 'Hong'

엔티티 직접 사용

기본 키 값

JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.

select count(m.id) from Member m	//엔티티의 PK를 사용
select count(m) from Member m		//엔티티를 직접 사용

다음과 같은 JPQL에서 실행된 SQL은 둘 다 똑같다.

String query = "select m from Member m where m = :member";
List resultList = entityManager.createQuery(query)
    .setParameter("member", member)
    .getResultList();

SQL

select m.*
from Member m
where m.id=?

 

외래 키 값

Team team = entityManager.find(Team.class, 1L);

String query = "select m from Member m where m.team = :team";
List resultList = entityManager.createQuery(query)
    .setParameter("team", team)
    .getResultList();

SQL은 다음과 같이 실행된다.

select m.*
from Member m
where m.team_id=?

Member와 Team 간에 묵시적 조인이 일어날 것 같지만 Member 테이블이 team_id 외래 키를 가지고 있으므로 묵시적 조인은 일어나지 않는다.

예를등러 m.team.name을 호출하면 묵시적 조인이 일어난다.  

 

Named 쿼리:정적 쿼리

  • 동적 쿼리
    - JPQL을 문자로 완성해서 직접 넘기는 것
    - 런타임에 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다.
    - 기존에 하던 방식
  • 정적 쿼리
    - 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을 Named 쿼리라 한다.
    - Named 쿼리는 한 번 정의하면 변경할 수 없는 정적 쿼리

Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱한다.

  • 오류를 빨리 확인할 수 있다.
  • 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다.
  • Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스의 조회 성능 최적화에 도움이 된다.
  • @NamedQuery 어노테이션을 사용하여 자바 코드에 작성하거나 xml 문서에 작성할 수 있다.

Named 쿼리를 어노테이션에 정의

@Entity
@NamedQuery(
        name = "Member.findByUsername",
        query = "select m from Member m where m.username = :username"
)
public class Member {
	...
}
List<Member> resultList = entityManager.createNamedQuery("Member.findByUsername", Member.class)
                .setParameter("username", "회원1")
                .getResultList();

NamedQuery를 두 개 이상 작성하고 싶다면 @NamedQueries를 사용하여 추가하면 된다.

@Entity
@NamedQueries({
        @NamedQuery(
                name = "Member.findByUsername",
                query = "select m from Member m where m.username = :username"
        ),
        @NamedQuery(
                name = "Member.count",
                query = "select count(m) from Member m"
        )
})
public class Member {
	...
}

 

@NamedQuery 어노테이션 정의

@Target({TYPE})
@Retention(RUNTIME)
public @interface NamedQuery {

    String name();	//Named쿼리 이름

    String query();	//JPQL 정의

    LockModeType lockMode() default NONE;	//쿼리 실행 시 락모드를 설정할 수 있다.

    QueryHint[] hints() default {};	// JPA 구현체에 쿼리 힌트를 줄 수 있다.
}
  • lockMode : 쿼리 실행 시 락을 건다
  • hints : JPA 구현체에게 제공하는 힌트

Named 쿼리를 XML에 정의

어노테이션을 작성하는 것이 직관적이고 편리하지만 Named 쿼리를 작성할 때는 XML로 작성하는 것이 더 편리하다.

왜냐하면 멀티 라인 문자를 다루는 것이 굉장히 귀찮기 때문이다.

"select " +
    "case t.name when '팀A' then '인센티브110%' " +
    "			when '팀B' then '인센티브120%' " +
    "			else '인센티브105%' end " +
"from Team t";

이런 불편함을 해결하려면 XML을 사용하는 것이 현실적이다.

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

그리고 정의한 xml을 인식하도록 META-INF/persistence.xml에 추가해야한다.

<persistence-unit name="jpabook">
    <mapping-file>META-INF/ormMember.xml</mapping-file>
    ...

'Spring JPA' 카테고리의 다른 글

스프링 데이터 JPA - 15  (0) 2021.07.31
객체 지향 쿼리 언어 - 14  (0) 2021.07.21
값 타입 - 12  (0) 2021.07.16
프록시와 연관관계 관리 - 11  (0) 2021.07.13
고급 매핑 - 10  (0) 2021.07.12

댓글