본문 바로가기
JPA

객체지향 쿼리 언어

by Heesu.lee 2021. 1. 25.

JPA 가 지원하는 다양한 쿼리 방법

  • JPQL
  • JPA Criteria
  • QueryDSL
  • 네이티브 SQL - 범주에서 벗어난 종속적인 방언 사용 시
  • 간혹 JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용

JPQL

  • JPA 는 SQL 을 추상화한 JPQL 이라는 객체 지향 쿼리 언어 제공
  • SQL 과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
  • JPQL 은 엔티티 객체를 대상으로 쿼리
    • 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리
  • SQL 은 데이터베이스 테이블을 대상으로 쿼리
    • SQL 을 추상화하여 특정 데이터베이스 SQL 에 의존 X
정리하면 객체 지향 SQL
※ 참고
JDBC 직접 사용, SpringJdbcTemplate 등 직접 DB 커넥션을 통해 쿼리 요청하는 경우,
JPA 와 함께 사용 중이라면, em.flush() 를 쿼리 요청 전에 한번 호출하여 쌓인 쿼리들을 반영시킬 필요가 있다.

JPQL 소개

  • JPQL 은 객체지향 쿼리언어로서 테이블 대상이 아닌 엔티티 객체를 대상으로 쿼리한다.
  • JPQL 은 SQL 을 추상화해서 특정데이터베이스 SQL 에 의존하지 않는다.
  • JPQL 은 결국 SQL 로 변환된다.

JPQL 문법

  • 엔티티 와 속성은 대소문자 구분
  • JPQL 키워드는 대소문자 구분하지 않음
  • 엔티티 이름 사용, 테이블 이름이 아니다. - @Entity(name=""), 엔티티 클래스 이름 (기본값)
  • 별칭은 필수로 작성해줘야 한다.

TypeQuery, Query

  • TypeQuery: 반환 타입이 명확할 때 사용
  • Query: 반환 타입이 명확하지 않을 때 사용

결과 조회 API

  • query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
  • query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환
리스트의 경우 빈 리스트를 반환하여 null 로부터 안전하지만,
단일 객체 반환인 경우 예외가 발생한다.
결과가 없으면 -
javax.persistence.NoResultException, 둘 이상인 경우 - javax.persistence.NonUniqueResultException

파라미터 바인딩

  • 이름 기준
    • SELECT m FROM Member m WHERE m.username=:username → setParameter("username", usernameParam)
  • 위치 기준 (사용하지 말자, 순서가 바뀌면 보장이 안됌)
    • SELECT m FROM Member m WHERE m.username=?1 → setParameter(1, usernameParam)

프로젝션

SELECT 절에 조회할 대상을 지정하는 것

프로젝션의 대상으로는 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입) 등이 있다.

  • SELECT m FROM Member m -> 엔티티 프로젝션 (영속성 컨텍스트에서 관리된다.)
  • SELECT m.team FROM Member m -> 엔티티 프로젝션 (join 을 되도록 명시하여 작성하자)
  • SELECT m.address FROM Member m -> 임베디드 타입 프로젝션
  • SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
  • DISTINCT로 중복 제거

프로젝션 - 여러 값 조회

SELECT m.username, m.age FROM Member m - Type 이 따로 지정할 수 없기 때문에

  • 1. Query 타입으로 조회
  • 2. Object[] 타입으로 조회
  • 3. new 명령어로 조회

new 명령어로 조회

  • 단순 값을 DTO 로 바로 조회
    • SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
  • 패키지 명을 포함한 전체 클래스 명 입력
  • 순서와 타입이 일치하는 생성자 필요
문자열의 한계를 지님 - 컴파일 시점에서 문제점 파악하기 어려움

페이징 API

JPA 는 페이징을 다음 두 API 로 추상화

  • setFirstResult(int startPosition) : 조회 시작 위치 (0 부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
페이징 사용 시 ORDER BY 와 함께 사용하는 것이 좋다.

조인

  • 내부 조인 [inner] join
  • 외부 조인 left [outer]
  • 세타 조인

조인 ON 절

ON 절을 활용한 조인 (JPA 2.1 부터 지원)

  • 조인 대상 필터링
    • 예시) SELECT m FROM Member m JOIN m.team t on t.name = "A"
  • 연관관계 없는 엔티티 외부 조인 (Hibernate 5.1 부터 지원 / 그 전엔 내부 조인만 가능)
    • 예시) SELECT m FROM Member m LEFT JOIN Team t on m.username = t.name

서브쿼리

  • JPAWHERE, HAVING 절에서만 서브 쿼리 사용 가능
  • SELECT 절도 가능(하이버네이트에서 지원)
  • FROM 절의 서브 쿼리는 현재 JPQL에서 불가능
    • 조인으로 풀 수 있으면 풀어서 해결

JPQL 타입 표현

  • 문자: ‘HELLO’, ‘She’’s’
  • 숫자: 10L(Long), 10D(Double), 10F(Float)
  • Boolean: TRUE, FALSE
  • ENUM: jpabook.MemberType.Admin (패키지명 포함)
  • 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)

JPQL 기본 함수

  • CONCAT
  • SUBSTRING
  • TRIM
  • LOWER, UPPER
  • LENGTH
  • LOCATE
  • ABS, SQRT, MOD
  • SIZE, INDEX - JPA 용도
만약 사용자 정의 함수를 사용하는 경우,
사용하는 DB 방언을 상속 받고, 사용자 정의 함수를 등록하고 사용해야 한다.

 

경로 표현식

. (점)을 찍어 객체 그래프를 탐색하는 것

select m.username -> 상태 필드
from Member m
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'

경로 표현식 용어 정리

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

경로 표현식 특징

  • 상태 필드 - 경로 탐색의 끝, 추가적인 탐색 불가
  • 단일 값 연관 경로 - 묵시적 내부 조인(inner join) 발생, 추가적인 탐색 가능
  • 컬렉션 값 연관 경로 - 묵시적 내부 조인 발생, 추가적인 탐색 불가
    • FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능 - from join t.member m

 

실제 운영 상에선 묵시적 내부 조인이 발생하지 않도록 구성해야 한다. - 명시적 조인을 사용하자 join 키워드 사용
튜닝 및 운영 하는데 있어 추적하기 어려운 부분이 발생한다.

 

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

  • 항상 내부 조인 사용
  • 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야 추가적인 탐색 가능
  • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL 의 FROM 절에 영향을 줌

경로 탐색 정리

  • 가급적 묵시적 조인 대신에 명시적 조인 사용
  • 조인은 SQL 튜닝에 중요 포인트
  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

페치 조인

  • SQL 조인 종류가 아니다
  • JPQL 에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회 하는 기능
  • join fetch 명령어 사용
  • 페치 조인 ::= [LEFT [OUTER] | INNER] JOIN FETCH 조인 경로

엔티티 페치 조인

예) 회원을 조회하면서 연관된 팀도 함께 조회 (SQL 한번에)

[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
명시적으로 동적인 타이밍에 결정할 수 있는 방식

컬렉션 페치 조인

일대다 관계, 컬렉션 페치 조인

[JPQL]
select t
from Team t join fetch t.memberswhere t.name = ‘A'

[SQL]
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_IDWHERE T.NAME = 'A'
컬렉션 페치 조인의 경우 중복된 결과값이 나온다. - 데이타 뻥튀기 발생

페치 조인과 DISTINCT

SQL 의 DISTINCT 는 중복된 결과 제거하는 명령

JPQL 의 DISTINCT 2가지 기능

  • SQL 에 DISTINCT 를 추가
  • 애플리케이션에서 엔티티 중복 제거

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

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

  • JPQL 은 결과를 반환할 때 연관관계 고려하지 않는다.
  • 단지 SELECT 절에 지정한 엔티티만 조회한다.
  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회 (즉시 로딩)
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

페치 조인의 특징과 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다. (가급적 사용 하지 말자, 여러 join fetch 하는 경우에만 사용)
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API (setFirstResult, setMaxResults)를 사용할 수 없다.
    • XtoOne 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
    • 하더라도 메모리에서 페이징하기 때문에 굉장히 위험함 → @BatchSize 키워드로 해결
  • 연관된 엔티티들은 SQL 한번으로 조회
  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
    • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
    • 최적화가 필요한 곳은 페치 조인 적용

페치 조인 정리

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

엔티티 직접 사용 사용 - 기본 키 값

JPQL 에서 엔티티를 직접 사용하면 SQL 에서 해당 엔티티의 기본 키 값을 사용

[JPQL]
select count(m.id) from Member m // 엔티티의 아이디를 사용
select count(m) from Member m // 엔티티를 직접 사용

[SQL]
(JPQL 둘다 같은 다음 SQL 실행)
select count(m.id) as cnt from Member m

엔티티 직접 사용 - 외래키 값

Team team = em.find(Team.class, 1L);
String qlString = “select m from Member m where m.team = :team”; 
List resultList = em.createQuery(qlString)
	.setParameter("team", team) .getResultList();
    

 

실행된 SQL

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

 

Named 쿼리

미리 정의해서 이름을 부여해두고 사용하는 JPQL

  • 정적 쿼리
  • 어노테이션, XML 에 정의
  • 애플리케이션 로딩 시점에 초기화 후 재사용
  • 애플리케이션 로딩 시점에 쿼리를 검증
@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();

Named 쿼리 환경에 따른 설정

  • XML 이 항상 우선권을 가진다.
  • 애플리케이션 운영 환경에 따라 다른 XML 을 배포할 수 있다.
public interface UserRepository extends JpaRepository<User, Long> {

  // 해당 방식도 익명의 Named 쿼리를 사용하는 방식
  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}

 

JPQL 벌크 연산

ID 를 하나를 사용하여 단건 조회를 제외한 나머지 연산(UPDATE, DELETE 등) 자체를 벌크 연산이라 생각하면 된다.

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

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

 

벌크 연산 주의

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리

  • 벌크 연산을 먼저 실행
  • 벌크 연산 수행 후 영속성 컨텍스트 초기화
어차피 벌크 연산 쿼리가 나가는 시점에 쌓인 쿼리가 요청되기에 그전 반영되지 않은 부분에 대해선 걱정하지 않아도 된다.
그렇지만 벌크 연산 이후 영속성 컨텍스트와 DB 반영값이 다르기에 벌크 연산 이후엔 영속성 컨텍스트를 초기화한 후 사용해야 한다.

 

 

참조

  • 해당 게시글은 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편을 바탕으로 작성되었습니다.

 

'JPA' 카테고리의 다른 글

Querydsl 사용 개요  (0) 2021.04.25
쿼리 메소드 기능  (0) 2021.01.26
프록시와 연관관계 관리  (0) 2021.01.20
값 타입  (0) 2021.01.19
상속 관계 매핑  (0) 2021.01.14

댓글