기억의 저장소
JPQL이란 무엇이고 왜 사용할까? 본문
JPQL이란 무엇이냐
JPA가 아무리 좋다고 하더라도 도메인에 따른 디테일한 쿼리는 생성하지 못한다. 공통적인 쿼리를 생성해줄뿐..
그래서 JPQL이란것을 만들게 되었다.JPA가 객체 중심 개발을 하는것처럼 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공하며 JPQL을 한마디로 정의하면 객체 지향 SQL이라 말할 수 있다. (SQL을 추상화해서 사용한다는것은 즉 객체지향적으로 개발한다는것이다) (SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지않게함)
그렇다고해서 SQL문법과 아예 다른것은 아니고 쿼리를 작성할때 객체명이나 객체필드명을 쓰는등의 방식이 조금 다르다.
문법
기본적인 문법
일반적인 SQL 문법과 같다 기본적인 SQL에서 제공되는 groupby having orderby등 모두 지원한다.
단 다른점이있는데 모두를 표현하는 문법과 기본적 구해오는 컬럼들을 표시해주는등이 다르다.
- SQL에서 모두를 표현하는것은 select문 사이에 * 를 집어 넣어 모두를 표시해주지만 JPQL은 Alias를 집어 넣어 모두를 표현해준다.
- 두번쨰로 SQL에서는 select age from member으로 표현하지만 JPQL에서는 무조건 Alias를 이용해 표현해야한다.
select m.age from Member m - 그리고 또다른점이 있는데 DB의 테이블 명을 사용하는게 아니라 엔티티 명을 입력하며
JPQL로 입력할때 JPQL 키워드는 대소문자 구분 하지않는다 (SELECT = select, FROM, where)
단 엔티티와 속성은 대소문자 구분한다. (자바클래스에서 대소문자 구분하듯이 (Member, age))
프로젝션
JPQL에서는 SELECT절에 조회할 대상을 지정하는것을 프로젝션이라한다.
프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)
RDB에서는 스칼라타입만 적을 수 있지만 엔티티 임베디드 타입 지원해준다.
SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
SELECT m.address FROM Member m -> 임베디드 타입 프로젝션
SELECT m FROM Member m -> 엔티티 프로젝션
SELECT m.team FROM Member m -> 엔티티 프로젝션(결과가 Team)
프로젝션에 여러가지 타입이 섞여나오면 결과를 어떻게 가져와야할까?
Query: 반환 타입이 명확하지 않을 때 사용(TypeQuery: 반환 타입이 명확할 때 사용)
Query타입으로 가져온 다음 결과를 Object로 가져와 Object[]로 변환하거나 Object[]로 가져오거나 해야한다.
또는 많이 사용되는 방법인데 new를 이용해 구현하는 방법이 있다.
단순 값을 DTO로 바로 조회 SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
( 패키지 명을 포함한 전체 클래스 명 입력 ,순서와 타입이 일치하는 생성자 필요 )
select 절에 m을 사용하면 Member 컬럼 모두를 조회해오는것이지만 아래와 같은경우는 엔티티의 식별자 값을 이용해 조회해온다.
select count(m) from Member m > 실제 sql select count(m.id) as cnt from Member m
select m from Member m where m = :member > 실제 sql select * from member where m.id = ?
경로 표현식
(점)을 찍어 객체 그래프를 탐색하는 것을 경로 표현식이라 말한다.
여기서 m.team과 같은걸 연관필드라 부른다.(연관경로들은 묵시적 조인이 일어나게됨)
이때 SELECT m.team FROM Member m 이렇게 작성하게되면 묵시적조인이 일어나게되는데
실무에서는 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려우므로 명시적 조인을 써야한다.
(연관되어있는 기본키와 외래키로조인이 일어난다 SELECT * FROM MEMBER m JOIN TEAM t on m.team_id = t.id)
SELECT m FROM Member m [INNER,LEFT,RIGHT] JOIN m.team t
조인
JPQL은 SQL모양과 비슷한 문법을 지원한다 물론 조인문도 같지만 다른 모양이 있다.
바로 아래와 같은 모양의 차이가 있다 JPQL은 객체스타일로 조인이 나가며 on을 적어주지않고
m.team t과 같이 참조 객체를 선언해주면된다.
SQL
SELECT * FROM MEMBER m JOIN TEAM t on m.team_id = t.id
JPQL
SELECT m FROM Member m [INNER,LEFT,RIGHT] JOIN m.team t
on절도 물론 사용할 수 있으며 보통 조인 대상 필터링할때 사용된다.
예) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
SQL:SELECT m., t. FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id (pk와 fk) and t.name='A'
또 사용하는경우가 있는데 바로 연관관계가 없을때 특정한 조건에 의해 조인을 하려할때 사용할 수 있다.
예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인( 회원과 팀이 관계가 없을떄를 가정한것)
JPQL:SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
SQL:SELECT m., t. FROM Member m LEFT JOIN Team t ON m.username = t.name
연관관계가 없을때 특정한 조건없이 N*M의 결과를 가지고오는 것을 세타조인이라하며 같다고 생각할 수 있지만 ON절을 사용하면 외부조인이 사용가능하다
select count(m) from Member m, Team t where m.username
https://www.inflearn.com/questions/119902/%EC%A7%88%EB%AC%B8%EC%9D%B4-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4
페치 조인
조인의 한종류이지만 JPQL에서만 사용되며 실무에서 엄청나게 중요하므로 따로 뺴보았다.
페치조인이란 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능이 있으며 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.members where t.name = ‘팀A'
[SQL] SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE [T.NAME] = '팀A'
페치조인 주의해야할점
1. 페치조인 대상에는 별칭을 주면 안된다.
JPA의 설계 사상은 객체 그래프를 탐색한다는 것은 연관된 엔티티 모두를 가져온다는 것을 가정하고 만들어 졌다.
그런데 아래와 같이 fetch join에 별칭을 붙이고 where절을 더해 필터해서 결과를 가져오게 되면 모든걸 가져온 결과와 비교하여 다른 갯수에 대해 정합성을 보장하지 않는다. 팀을 조회하는 상황에서 멤버가 5명인데 3명만 조회한 경우, 3명만 따로 조작하는 것은 몹시 위험하다.
String query = "select t from Team t join fetch t.members as m where m.age > 10";
2. 둘이상의 컬렉션은 페치조인 할 수 없다.
둘이상의 컬렉션이 만나게되면 n*n이되버려 결과가 예상할수 없게된다.
3.컬렉션을 페치조인 하면 페이징 API를 사용할 수없다.
일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다. 그런데 왜 컬렉션은 불가능할까?
1:N관계의 페치조인을 하게되면 데이터베이스에서는 페이징을 할 수 없다. 왜냐하면 데이터 뻥튀기가 되있기때문이다.
페치조인은 연관되있는 데이터가 지연로딩이든 상관없이 한번에 가져오는 데이터다.
만약 select t from Team t join fetch t.members라는 쿼리가 나가게 되면 어떤 값을 예상할수 있을까?
팀A가 있고 멤버 두명이 있다고했을때 팀A는 한개이니 한줄을 예상하지만 멤버가 두명이라 2개의 row를 가지게된다. 이걸 데이터 뻥튀기라한다. (JPA입장에서는 디비에서 두개의 row를 주면 두개의 row를 받아야한다. )
왜안되냐 왜 사용하면안되냐?
만약 페이징을 10으로하는데 팀A는 11명의 멤버가 있다. 당연히 데이터 뻥튀기가 되어 11개의 row가 되는데
이때 페이징을하개되면 팀A에는 10명의 멤버만 있는거 처럼 처리되어 정합성이 깨지게된다.
그럼 하이버네이트는 어떻게 하냐?
정합성을 지키기 위해 데이터가 100만건이라면 메모리에 모두올려 페이징을해버린다.
하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험함) 해버린다.
https://www.inflearn.com/questions/235551
그럼 어떻게 해야할까?
1. 일대다를 다대일로 방향을 전환해서 해결한다!
String query = "select t From Team t join fetch t.members m";
를 아래와 같이 변경한다.
String query = "select m From Member m join fetch m.team t";
2. fetch join을 제거하고 @BatchSize를 이용한다.
select t from Team t으로 놨두는데 members는 지연로딩이니 10개나오면 select문이 10개가 더생성이된다.(N+1문제)
그럼 어떻게하냐?
@BatchSize 100을 설정해두면
Team을가져올때 members는 지연로딩 상태인데 지연로딩된것을 끌고올떄 어떻게하냐면 내팀뿐이아니라 조회됬던 팀 100개까지를 in 쿼리를 통해 한번에 조회해옴 N+1을 1+1로 줄이는것
정리
모든것을 페치조인으로 해결할 수는 없다.
페치조인은 객체그래프를 유지할때 사용하며 효과적이며 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다. (객체 그래프 유지란 객체 그래프 탐색)
https://ym1085.github.io/jpa/JPA-%ED%8E%98%EC%B9%98%EC%A1%B0%EC%9D%B8-%EB%A7%88%EB%AC%B4%EB%A6%AC/
파라미터 바인딩
아래와 같이 :를 사용한후 내가전달한 파라미터명을입력하면된다.
SELECT m FROM Member m where m.username=:username
List<Member> getMember(String username)
페이징
JPA는 페이징을 다음 두 API로 추상화한다.
setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
setMaxResults(int maxResult) : 조회할 데이터 수
//페이징 쿼리
String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setFirstResult(10)
.setMaxResults(20)
.getResultList();
10개부터 가져오며 최대 20개 가져온다는것이다.
MySql은 sql로 변형될때 offset과 Limit으로 바뀌게된다.
다른 RDB에서 방언이 다르므로 다른 쿼리를 내보내주게되는데 개발자들은 JPA 추상화 덕분에 쉽게 개발할 수 있게되었다.
(Spring Data Jpa써도 페이징 쉽게되는데 결과적으로 보면 JPA가 다해주는거)
벌크연산
쿼리 한 번으로 여러 테이블 로우 변경(엔티티)하는것을 벌크연산이라 말한다.
순수 JPA에서는 UPDATE, DELETE 지원
구현체인 하이버네이트에서는 INSERT(insert into .. select, 하이버네이트 지원)
(셀렉트해서 인설트한다고하는데 그건 하이버네이트가 지원)
주의해야할것이 있는데 벌크연산은 영속성컨텍스트를 생각하지 않고 직접적으로 쿼리를 날리는거기때문에 벌크연산 수행 후 영속성 컨텍스트 초기화를 해주는것이 좋다.
순수 JPA
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(); //executeUpdate()의 결과는 영향받은 엔티티 수 반환
부록
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요 한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
세가지 방법이있는데
페치조인으로 엔티티를 조회해온다 그걸 그대로쓴다
두번쨰 페치조인한다음에 dto로 바꿔서 반환한다
세번쨰 처음부터 jpql에 new를 통해 새로운 dto로 만든다
'Spring > JPA' 카테고리의 다른 글
JPA란 뭘까? 왜 나오게 되었을까? 왜 사용할까? (0) | 2023.07.18 |
---|