JPQL은 가장 중요한 객체지향 쿼리 언어이다!
10.1 객체지향 쿼리 소개
10.1.1 JPQL 소개
JPQL(Java Persistence Query Language)
- 엔티티 객체를 조회하는 객체지향 쿼리이다
- SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다
- SQL보다 간결하다
10.1.2 Criteria 쿼리 소개
Criteria는 JPQL을 생성하는 빌더 클래스이다!
- Criteria의 장점은 문자가 아닌 query.select(m).where(...) 처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이다
- 그렇기 때문에 컴파일 시점에 오류를 발견할 수 있다
- 동적 쿼리를 작성할 때 유용하다
- Criteria가 가진 장점이 많지만 모든 장점을 상쇄할 정도로 복잡하고 장황하다. 따라서 사용하기 불편한 건 물론이고, Criteria로 작성한 코드도 한눈에 들어오지 않는다는 단점이 있다
10.1.3 QueryDSL 소개
QueryDSL도 Criteria 처럼 JPQL 빌더 역할을 한다!
- QueryDSL의 장점은 코드 기반이면서 단순하고 사용하기 쉽다
- QueryDSL도 어노테이션 프로세서를 사용해서 쿼리 전용 클래스를 만들어야 한다
10.1.4 네이티브 SQL 소개
JPA가 SQL을 직접 사용할 수 있는 기능이다!
- JPQL을 사용해도 가끔은 특정 데이터베이스에 의존하는 기능을 사용해야 할 때 사용한다
- 특정 데이터베이스에 의존하는 SQL을 작성해야 한다는 단점이 있어서, 데이터베이스를 변경하면 네이티브 SQL도 수정해야한다
10.2 JPQL
10.2.1 기본 문법과 쿼리 API
JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다
엔티티를 저장할 때는 EntityManager.persist() 메서드를 사용하면 되므로 INSERT문은 없다
SELECT 문
SELECT m FROM MEMBER AS m where m.username = 'Hello'
- 엔티티와 속성은 대소문자를 구분한다
- Member는 클래스 명이 아니라 엔티티 명이다
- 별칭은 필수이다
TypedQuery, Query
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class");
- TypedQuery 객체 : 반환할 타입을 명확하게 지정할 수 있다
Query query = em.createQuery("SELECT m.username, m.age from Member m");
- Query 객체 : 반환 타입을 명확하게 지정할 수 없다
10.2.2 파라미터 바인딩
JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩도 지원한다
이름 기준 파라미터
String usernameParam = "User1";
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m where m.username=:username", Member.class);
query.setParameter("username",usernameParam);
- 앞에 : 를 사용한다
위치 기준 파라미터
List<Member> members = em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class)
.setParameter(1, usernameParam)
.getResultList();
- ? 다음에 위치 값을 주면 된다
- 위치 값은 1부터 시작된다
*이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.
10.2.3 프로젝션
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션(projection)이라 하고 [SELECT {프로젝션 대상} FROM] 으로 대상을 선택한다
엔티티 프로젝션
SELECT m FROM Member m //회원
SELECT m.team FROM Member m //팀
- 원하는 객체를 바로 조회한 것인데 컬럼을 하나하나 나열해서 조회해야 하는 SQL 과는 차이가 있다
- 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다
임베디드 타입 프로젝션
String query = "SELECT a FROM Address a";
- 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.
- 위 코드는 임베디드 타입인 Address를 조회의 시작점으로 사용해서 잘못된 쿼리이다
String query = "SELECT o.address FROM Order o";
List<Address> addresses = em.createQuery(query, Address.class).getResultList();
- 위 코드는 Order 엔티티가 시작점이다
- 이렇게 엔티티를 통해서 임베디드 타입을 조회할 수 있다
임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다
스칼라 타입 프로젝션
List<String> usernames = em.createQuery("SELECT username FROM Member m", String.class).getResultList();
- 숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라고 한다
- 위 쿼리는 전체 회원의 이름을 조회하는 쿼리이다
여러 값 조회
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();
Iterator iterator = resultList.iterator();
while(iterator.hasNext()){
Object[] row = (Object[])iterator.next();
String username = (String)row[0];
Integer age = (Integer) row[1];
}
- 프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신에 Query를 사용해야 한다
- 스칼라 타입뿐만 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있다
- 조회한 엔티티는 영속성 컨텍스트에서 관리된다
NEW 명령어
TypedQuery<UserDTO> query =
em.createQuery("SELECT new jpabook.jpql.userDTO(m.username, m.age) FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
- SELECT 다음에 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있다
- 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다
10.2.5 집합과 정렬
집합함수
함수 | 설명 |
COUNT | 결과 수를 구한다. 반환 타입: Long |
MAX, MIN | 최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용한다 |
AVG | 평균값을 구한다. 숫자타입만 사용할 수 있다. 반환타입: Double |
SUM | 합을 구한다. 숫자타입만 사용할 수 있다. |
GROUP BY, HAVING
- GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다
- HAVING은 GROUP BY 와 함께 사용하는데 GROUP BY 로 그룹화한 통계 데이터를 기준으로 필터링한다
정렬(ORDER BY)
orderby_절 :: = ORDER BY {상태필드 경로 | 결과 변수 [ASC | DESC]} +
select m from Member m order by m.age DESC, m.username ASC
- ORDER BY 는 결과를 정렬할 때 사용한다
10.2.6 JPQL 조인
내부조인
- 내부 조인은 INNER JOIN을 사용한다 (INNER 생략가능)
String teanName = "팀A";
String query = "SELECT m FROM Member m INNER JOIN m.team t"
+"WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
.setParameter("teamName", teamName)
.getResultList();
- JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다.
- 여기서는 m.team이 연관 필드인데 연관 필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말한다
외부조인
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
- 외부 조인은 기능상 SQL의 외부 조인과 같다
- OUTER 는 생략 가능해서 보통 LEFT JOIN으로 사용한다
컬렉션 조인
일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다
- [회원 → 팀] 으로의 조인은 다대일 조인이면서 단일 값 연관 필드(m.team)를 사용한다
- [팀 → 회원] 은 반대로 일대다 조인이면서 컬렉션 값 연관 필드(m.members)를 사용한다
SELECT t, m FROM Team t LEFT JOIN t.members m
- 여기서 t LEFT JOIN t.members 는 팀과 팀이 보유한 회원 목록을 컬렉션 값 연관 필드로 외부 조인했다
세타 조인
- WHERE 절을 사용해서 세타조인을 할 수 있다
- 세타 조인은 내부 조인만 지원한다
- 세타 조인을 사용하면 전혀 관계없는 엔티티도 조인할 수 있다
10.2.7 페치 조인
- 페치 (fetch)조인은 JPQL 에서 성능 최적화를 위해 제공하는 기능이다
- 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능인데 join fetch 명령어로 사용할 수 있다
엔티티 페치 조인
select m from Member m join fetch m.team
- 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회하는 JPQL 이다
- 페치 조인은 별칭을 사용할 수 없다
컬렉션 패치 조인
- User 엔티티가 있고, 이 User는 여러 개의 Order를 가지고 있다고 가정해보자 (1:N 관계)
- User를 조회할 때, 해당 User가 가진 모든 Order도 한 번에 가져오고 싶을 때 컬렉션 패치 조인을 사용하면 된다
페치 조인과 DISTINCT
- 컬렉션 패치 조인을 사용할 경우, 결과 데이터에 중복이 발생할 수 있다
- 이를 해결하기 위해 DISTINCT 키워드를 사용하거나 DTO로 매핑하는 방법을 사용해야 한다
페치 조인과 일반 조인의 차이
//내부 조인 JPQL
select t
from Team t join t.members m
where t.name = '팀A'
- 팀만 조회하고 조인했던 회원은 전혀 조회하지 않는다
- JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다
페치 조인의 특징과 한계
- 글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요하면 패치 조인을 적용하는 것이 효과적이다
- 페치 조인을 사용하면 연관된 엔티티를 쿼리 시점에 조회하므로 지연로딩이 발생하지 않는다
- 따라서 준영속 상태에서도 객체 그래프를 탐색할 수 있다
한계
- 페치 조인 대상에서는 별칭을 줄 수 없다
- 둘 이상의 컬렉션을 페치할 수 없다
- 컬렉션을 페치조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다
10.2.8 경로 표현식
경로 표현식의 용어 정리
- 상태 필드(status field)
- 단순히 값을 저장하기 위한 필드
- 연관 필드(association field)
- 연관관계를 위한 필드, 임베디드 타입 포함
- 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티
- 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션
@Entity
public class Member{
@Id @GeneratedValue
private Long id;
@Column(name = "name")
private String username; //상태 필드
private Integer age; //상태 필드
@ManyToOne(...)
private Team team; //연관 필드(단일 값 연관 필드)
@OneToMnay(...)
private List<Order> orders; //연관 필드(컬렉션 값 연관 필드)
}
3가지 경로 표현식이 있다
- 상태 필드: t.username, t.age
- 단일 값 연관 필드 : m.team
- 컬렉션 값 연관 필드 m.ordres
경로 표현식과 특징
select m.username, m.age from Member m
- 상태 필드 경로: 경로 탐색의 끝이다. 더는 탐색할 수 없다
select o.member from Order o
- 단일 값 연관 경로: 묵시적으로 내부 조인이 일어난다. 단일 값 연관 경로는 계속 탐색할 수 있다
- 명시적 조인: JOIN 을 직접 저어주는 것
- 묵시적 조인: 경로 표현식에 의해 묵시적으로 조인이 일어나는 것
select t.members from Team t //성공
selec t.members.username from Team t //실패
select m.username from Team t join t.members m //성공
- 컬렉션 값 연관 경뢰 묵시적으로 내부 조인이 일어난다. 더는 탐색할 수 없다. 단 FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있다
10.2.15 Named 쿼리: 동적 쿼리
JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있다
- 동적 쿼리
- em.createQuery("select ... ") 처럼 문자로 완성해서 직접 넘기는 것
- 런타임에 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다
- 정적 쿼리
- 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는 것
- Named 쿼리는 한 번 정의하면 변경할 수 없는 정적인 쿼리다
Named 쿼리
- 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해둔다
- 오류를 빨리 확인할 수 있다
- 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다
- 데이터베이스의 조횟 성능 최적화에도 도움이 된다
Named 쿼리를 어노테이션에 정의
@Entity
@NamedQuery(
name = "User.findByName",
query = "SELECT u FROM User u WHERE u.name = :name"
)
public class User {
@Id
private Long id;
private String name;
}
TypedQuery<User> query = em.createNamedQuery("User.findByName", User.class);
query.setParameter("name", "소윤");
List<User> result = query.getResultList();
'JPA 프로그래밍' 카테고리의 다른 글
Ch12. 스프링 데이터 JPA (0) | 2025.01.22 |
---|---|
Ch10. 객체 지향 쿼리 언어 (10.5 네이티브 SQL, 10.6 객체지향 쿼리 심화) (0) | 2025.01.21 |
Ch10. 객체지향 쿼리 언어 (10.4 QueryDSL) (0) | 2025.01.21 |
Ch10. 객체지향 쿼리 언어 (10.3 Criteria) (1) | 2025.01.20 |
Ch.9 값 타입 (1) | 2025.01.16 |