[JPA] 객체지향 쿼리 언어(JPQL) 이해하기
- [ Backend ]/Spring DB, JPA
- 2022. 1. 17.
개요
DB에 접근하여 데이터를 수정하려면 쿼리를 DB로 날려주어야 한다. JPA를 사용하면 기본적인 SQL문이 자동으로 나가게 되긴 하지만, 직접 쿼리문을 작성하여 쿼리를 보내야 할 때를 위해 SQL을 추상화하여 만든 객체지향 SQL이 JPQL이다.
JPQL은 SQL을 추상화했기 때문에 DB의 종류에 의존적이지 않다는 특징이 있다. (DB에 맞게 변환하는것은 jpa과 hibernate의 몫)
기본 활용법
JPQL의 문법은 SQL과 유사하다. SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN등 SQL 문법을 모두 지원한다. 다만 테이블을 대상으로 쿼리를 작성하는 일반 SQL문과 다르게 JPQL은 jpa의 사용에 맞추어 엔티티 객체를 대상으로 쿼리를 작성할 수 있다.
SELECT문 예시
select m from Member m where m.age>18
위 쿼리에서 Member는 테이블이 아니라 엔티티를 가리키는 것이며, m은 별칭을 나타낸다. 또한 *을 사용하여 데이터를 불러오는 SQL과 다르게, m은 Member 객체 자체를 말한다.
타입 표현
문자: 'String'
숫자: 10L, 10D, 10F
boolean: TRUE, FALSE
enum: [패키지 경로+클래스명]
사용가능한 함수
CONCAT
SUBSTRING
TRIM
LOWER, UPPER
LENGTH
LOCATE
ABS, SQRT, MOD
SIZE, INDEX
경로 표현식
경로 표현식이란 .(점)을 활용하여 객체 그래프를 탐색하는 것을 말한다.
select m.username
from Member m
join m.team t
join m.orders o
where t.name = 'teamA'
여기서는 m.username, m.team, m.orders라는 세 개의 경로 표현식이 있는데, 이 세개의 필드은 모두 다른 속성을 가지며, 내부적으로 동작하는 방식에 차이를 보인다.
1. 상태 필드
m.username은 상태 필드라고 불리며, 단순히 값을 나타내는 필드이다.
2-1. 단일 값 연관 필드
m.team은 연관관계를 갖는 엔티티를 나타내며, 즉 team은 엔티티를 나타낸다.
단일 값 연관 필드의 경우, 이너 조인이 추가로 발생하여 연관된 테이블을 조인 연산을 통해서 조회해온다. 암시적으로 추가 쿼리가 나갈 수 있기 때문에 성능에 악영향을 미칠 수 있다.
2-2. 컬렉션 값 연관 필드
m.orders는 연관관계를 위한 필드이며, orders는 일대다나 다대다 연관관계일 경우 조회되는 엔티티의 컬렉션을 나타낸다.
컬렉션 값 연관 필드의 경우에도 이너 조인이 추가로 발생한다. 이 역시 암시적으로 추가 쿼리가 나갈 수 있기 때문에 성능에 악영향을 미칠 수 있다.
결론
암시적 조인을 사용하지 말고, 직접 명시적으로 조인해서 사용하자. 보이지 않는 쿼리는 디버깅에 매우 좋지 않다.
Java 객체로 받아 사용하기
- TypeQuery
반환타입을 명시할 수 있을때 TypeQuery 객체를 사용해서 받아줄 수 있다.
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
- Query
반환타입이 불명확할 때 사용하며, TypeQuery와 다르게 클래스타입을 명시해줄 수 없다.
Query query = em.createQuery("select m.username, m.age from Member m");
TypeQuery와 Query형 객체는 작성한 jpql을 실행시키기 위해 만드는 쿼리 객체이다.
- .getResultList();
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
리턴값이 하나 이상일때 결과를 컬렉션으로 반환받을 수 있다.
- .getSingleResult();
Member members = em.createQuery("select m from Member m", Member.class).getSingleResult();
리턴값이 단일값일 때(id를 특정하여 반환받을 경우 등) 단일 객체를 반환. 만약 결과가 없거나 둘 이상이면 예외가 발생하기 때문에 이 점은 유의해야 한다.
- 파라미터 바인딩
TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = :username", Member.class);
query.setParameter("username", "member1");
Member member = query.getSingleResult();
System.out.println(member.getUsername()); // "member1"
setParameter() 메서드를 통해서 파라미터를 동적으로 바인딩해줄 수 있다.
프로젝션
프로젝션이란 select절에 조회할 대상을 지정하는 것을 의미한다.
- 엔티티 프로젝션
select m from Member m;
select m.team from Member m
= select t from Member m join Team m.team t
-> inner join을 통해 연관된 team 정보를 가져온다.
-> 엔티티 프로젝션의 경우 조회된 엔티티들은 자동으로 영속성 콘텍스트의 관리 대상이 된다.
-> join을 통해 엔티티 프로젝션이 이루어지는 경우, 두번째 쿼리와 같이 상세하게 적어주는 것이 좋다. (sql문의 예상을 위해)
- 임베디드 타입 프로젝션
select m.address from Member m
- 스칼라 타입 프로젝션
select m.username, m.age from Member m
-> 스칼라 타입과 같이 여러 값을 조회해야 할 때 여러가지 방법으로 받을 수 있다.
1. Query 타입 조회
Query query = em.createQuery("select m.username, m.age from Member m");
2. Object[] 타입 조회
List rstList = em.createQuery("select m.username, m.age from Member m");
Object obj = resultList.get(0);
Object[] rst = (Object[]) obj;
//username == rst[0];
//age == rst[1];
==================================== OR ===========================================
List<Object[]> rstList = em.createQuery("select m.username, m.age from Member m");
Object[] rst = rstList.get(0);
//username == rst[0];
//age == rst[1];
3. new 명령어 조회
List <Object[]> resultList = em.CreateQuery(
"SELECT new 패키지명.MemberDTO(m.username, m.age) FROM Member m" //new operation
).getReusltList();
객체 생성시 생성자를 호출하듯 객체 형태로 받아내는 방법이 있다. 패키지 경로를 적어주어야 한다는 점 빼고는 제일 효율적인 방법이다.
페이징
oracle이나 mssql에서의 복잡한 페이징 쿼리와 다르게, jpa는 페이징을 다음 두 개의 api로 추상화했다.
etFirstResult(int startPosition); // 몇 번째부터 시작하는지
setMaxResults(int maxResult); // 몇 개나 가져오는지
em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(5) // 인덱스 5부터 (0부터 시작이다.)
.setMaxResults(10) // 10개
.getResultList(); // 가져오기
-> Member테이블에서 나이(m.age)순으로 내림차순으로(desc) 인덱스 5부터 10개를 가져온다.
조인
select t from Member m [inner] join Team m.team t;
List<Member> rstList = em.createQuery(query, Member.class).getResultList();
이렇게 앞서 프로젝션에서 설명한 것처럼 별칭을 넣어 작성해주면 되는데, on절을 활용하여 필터링 조건을 추가해 줄 수도 있다.
예시 1) 회원과 팀을 outer join하면서 팀명이 A인 팀만 조인
SELECT m,t FROM Member m LEFT JOIN m.team t on t.name = 'A'
예시 2) 회원의 이름과 팀의 이름이 같은 대상 조인 (연관관계가 없는 엔티티 조인)
SELECT m,t FROM Member m LEFT JOIN Team t on m.username = t.name
패치(Fetch) 조인
fetch join은 실제로 존재하는 SQL 문법은 아니고, JPQL에서 성능 최적화를 위해 연관된 엔티티나 컬렉션을 SQL 한 번에 조회할 수 있는 기능이다.
select m from Member m join fetch m.team
위와 같이 사용할 수 있는데, 이를 쿼리로 실행시키면
select m.*, t.* from member m inner join team t on m.team_id=t.id
이런 식으로 변환된다. 일반적인 조인과의 차이는 member뿐만 아니라 team도 한꺼번에 가져온다는 것. N+1 문제 발생을 예방할 수 있다.
*주의*
1. 단, 일대다 fetch join을 할 경우에는 데이터 뻥튀기를 주의하자. distinct 옵션을 사용하면 일대다 조인으로 인해 컬렉션을 조회할 경우 중복을 제거할 수 있다.
2. fetch join 대상에는 별칭을 줄 수 없다. 기술적으로는 가능하지만, 관례적으로 금지되어 있다.
3. 둘 이상의 컬렉션은 fetch join할 수 없다(곱하기 연산이기 때문에 데이터가 기하급수적으로 늘어날 수 있다).
4. 일대다 연관필드의 경우 페이징 API(setFirstResult(), setMaxResults())를 사용할 수 없다. 기술적으로는 사용 가능하지만, 중복이 발생한 데이터를 페이징하는 것은 불가능하기 때문에 사용하지 말자.
정리
여러 테이블을 조인해서, 엔티티의 기본 모양이 아닌 전혀 다른 결과를 내야 한다면 fetch join보다는 일반 조인을 사용하고 필요한 데이터들만 DTO로 조회하는 것이 효과적이다.
서브쿼리
JPQL도 아래와 같이 서브쿼리를 지원하며, 쿼리의 성능을 조금이라도 더 향상시키는 방법으로 서브쿼리를 활용할 수 있다. 단, 현재 JPQL에서는 from절의 서브쿼리를 작성할 수 없다는 한계가 있다.
예시 1) 나이가 평균보다 많은 회원을 서브쿼리를 통해서 조회
select m from Member m
where m.age > (select avg(m2.age) from Member m2)
예시 2) 한 건이라도 주문을 한 고객을 조회
select m from Member m
where (select count(o) from Order o where m = o.member) > 0
'[ Backend ] > Spring DB, JPA' 카테고리의 다른 글
[JPA] 순환참조, 무한참조 (0) | 2022.07.26 |
---|---|
[Spring JPA] 변경감지 vs 병합 (0) | 2022.02.02 |
[JPA] 값 타입 (0) | 2022.01.15 |
[JPA] 프록시, 연관관계 관리 - 즉시/지연로딩, 영속성 전이, 고아 객체 (0) | 2022.01.13 |
[JPA] 연관관계 매핑(3) - 상속관계 매핑 (0) | 2022.01.11 |