- JPA를 사용하면 애플리케이션 개발자는 엔티티 객체를 중심으로 개발하고 데이터베이스에 대한 처리는 JPA에게 맡겨야 함 ⇒ 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 함
- 애플리케이션이 필요한 데이터만 데이터베이스에서 불러오려면 검색 조건이 포함된 SQL을 사용해야 하는데 JPA는 JPQL(Java Persistence Query Language)로 이 문제를 해결함
- JPQL은 엔티티 객체를 대상으로 쿼리함. 쉽게 이야기해서 클래스와 필드를 대상으로 쿼리함. JPQL은 데이터베이스 테이블을 전혀 알지 못함
사용법Query , TypedQuery ( 페이징 쿼리) — EntityManager를 사용한 JPQL 작성파라미터 바인딩프로젝션NEW 명령어집합과 정렬GROUP BY, HAVING, ORDER BYIn절페치 조인FunctionsOperation객체지향 쿼리 심화벌크 연산JPQL로 조회한 엔티티와 영속성 컨텍스트find() vs JPQL성능 개선JPA exists 쿼리 성능 개선JPQL에서 limit 을 사용하는 방법
사용법
- 엔티티와 필드값은 대소문자를 구분함(Member, username). SELECT, FROM, AS 같은 JPQL 키워드는 대소문자 구분 안함
- 별칭 필수임 Member AS m 과 같이.
SELECT m.username FROM Member m
Query , TypedQuery ( 페이징 쿼리) — EntityManager를 사용한 JPQL 작성
@Repository public class SearchAccountBookRepository { @PersistenceContext private EntityManager em; public List<Expenditure> searchExpenditures() { TypedQuery<Expenditure> query = em.createQuery( "select e from Expenditure e "); query.setParameter(key, params.get(key)); // Jpql로 페이징 쿼리 작성 query.setFirstResult((int)pageRequest.getOffset()); query.setMaxResults(pageRequest.getSize()); query.getResultList()
- 반환할 타입을 명확하게 지정할 수 있으면 TypeQuery
- 반환 타입을 명확하게 지정할 수 없으면 Query 객체를 사용하면 됨
- 페이징 쿼리 : JPQL 은 limit 과 offset을 지원하지 않아서 위와 같이 작성해야 함 (참고 : Pagination with Jpa and Hibernate)
- 위와 같은 방식으로 페이징 할 때 collection을 join fetch 하게 되면 MaxResult의 개수에 영향을 미쳐서 생각과 다른 결과가 나오게 됨
- 그래서 쿼리를 따로 두개로 구성해야함. 페이징 적용할 엔티티에 대해서 가져온 다음에 해당 엔티티로 collection 가져와야함
파라미터 바인딩
- 이름 기준 파라미터
SELECT m FROM Member m where m.username =:username
- 위치 기준 파라미터
SELECT m FROM Member m where m.username = ?1
프로젝션
- SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라고 함
- 임베디드 타입은 조회의 시작점(FROM)이 될 수 없음
NEW 명령어
- SELECT 다음에 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있음
SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
- 주의점
- 패키지 명을 포함한 전체 클래스 명을 입력해야 함
- 순서와 타입이 일치하는 생성자가 필요
집합과 정렬
- count
- MAX, MIN
- AVG
- SUM
GROUP BY, HAVING, ORDER BY
SELECT t.name, COUNT(m.age) as cnt from Member m LEFT JOIN m.team t GROUP BY t.name ORDER BY cnt
JPQL 조인
INNER JOIN
- JPQL도 조인을 지원하는데 SQL 조인과 기능은 같고 문법만 약간 다름
SELECT m FROM Member m INNER JOIN m.team t
- 일반 SQL 조인과 달리 JPQL에서는 Member의 안에 있는 연관관계를 이용해서 Join을 실행함(
JOIN
m.team
.JOIN Team
- INNER는 생략 가능
- 조인한 두 개의 엔티티를 조회하려면 다음과 같이 JPQL 작성
SELECT m,t FROM Member m JOIN
m.team
t
외부 조인
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
컬렉션 조인
- 일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 함
SELECT t, m FROM Team t LEFT JOIN t.members m
JOIN ON절
- SQL에서의 JOIN ON 과는 다름. 거기서는 연관관계를 어떻게 맺느냐 할 때 ON을 쓰지만 여기서는 조인 대상을 필터링하고 싶을 때 사용함
SELECT m,t FROM Member m left join m.team t on t.name = ‘A’
- [SQL]
SELECT m.*, t.* FROM Member b LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name=’A’
In절
@Query("SELECT * FROM Employee as e WHERE e.employeeName IN :names") List<Employee> findByEmployeeName(@Param("names") List<String> names);
페치 조인
페치조인은 SQL 한번으로 연관된 여러 엔티티를 조회할 수 있어서 성능 최적화에 상당히 유용함. 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적. 반면에 여러 테이블으르 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 억지로 페치 조인으르 사용하기보다는 여러 테이블에서 필요한 필드들만 조회해서 DTO로 반환하는 것이 더 효과적일 수 있음
- JPQL을 사용해서 entity를 조회할때, eagerFetch인 관계가 있는데 join fetch로 가져오지 않으면 select 문이 여러번 날아가게 됨
- 예외 :
findById()
는 Eager Fetch면 따로 명시하지 않아도 한번에 다 가져옴 - 자동 생성되는 쿼리메서드로는 Eager Fetch 한번에 inner join 으로 가져오진 않고 쿼리 한번 더 날아감
- SQL에서 이야기하는 조인의 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능임
- 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능으로 join fetch 명령어로 사용 가능
엔티티 페치 조인
SELECT m FROM Member m join fetch m.team
컬렉션 페치 조인
SELECT t FROM Team t join fetch t.members where t.name=’팀A’
- 컬렉션 페치 조인은 하나의 팀에 여러 개의 멤버가 붙어 있으므로 팀의 객체 개수가 증가하게 됨
ID | NAME |
1 | 팀A |
ID | TEAM_ID | NAME |
1 | 1 | 회원1 |
2 | 1 | 회원2 |
String jpql = "select t from Team t join fetch t.members where t.name='팀A'"; for(Team team : teams){ System.out.println(team); for(Member member : team.getMembers()){ System.out.println(member); } } /* Team@0x100 Member@0x200 Member@0x300 Team@0x100 Member@0x200 Member@0x300 */
페치 조인과 DISTINCT
- JPQL의 DISTINCT는 SQL에 DISTINCT를 추가하는 것은 물론이고 애플리케이션에서 한 번 더 중복을 제거함
- 바로 위의 컬렉션 페치 조인에서 distinct를 붙이게 되면 sql 상에서는 효과가 없지만(row가 다르기에) 애플리케이션에서는 중복이 제거가 됨
SELECT distinct t FROM Team t join fetch t.members where t.name=’팀A’
페치 조인과 일반 조인의 차이
//내부 조인 JPQL select t from Team t join t.members 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';
- 프로젝션 부분에 t만 있기 때문에 team만 조회하게 됨. members는 안가져옴
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';
- 프로젝션에 t만 있다 하더라도 페치 조인을 이용하여 데이터를 가져오기에 연관된 엔티티도 함께 조회함
페치 조인의 특징과 한계
- 페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화 할 수 있음
- 엔티티에 직접 적용하는 FetchType은 글로벌 로딩 전략이고, 페치 조인은 이보다 우선 됨
- 될 수 있으면 글로벌 로딩 전략은 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적
한계
- 페치 조인 대상에는 별칭을 줄 수 없다 ⇒ nested fetch join이 불가능함 [ 참고 StackOverflow ]
- JPA Spec은 fetch join 에 alias를 줄 수 없어서 nested join fetch 이 불가 EclipseLink JPA Provider를 사용하면 가능
- A를 가져오면서 B도 가져오고, B에 연관관계가 있는 C도 가져오고 싶어. 이 상황은 안된다.
- 둘 이상의 컬렉션을 페치할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다
- 컬렉션(일대다)이 아닌 단일 값 연관 필드(일대일, 다대일)들은 페치 조인을 사용해도 페이징 API를 사용할 수 있음
경로 표현식
select m.username from Member m join m.team t join m.orders o where t.name = '팀A'
- 상태 필드 경로: 경로 탐색의 끝. 더 탐색할 수 없음
- 단일 값 연관 경로(연관관계) : 묵시적으로 내부 조인이 일어남. 단일 값 연관관계는 계속 탐색할 수 있음
- 컬렉션 값 연관 경로: 묵시적으로 내부 조인이 일어남. 더는 탐색할 수 없다. 단 FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있음
경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인임
- 컬렉션은 경로 탐색의 끝. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 함
- 경로 탐색은 주로 SELECT, WHERE절(다른 곳도 사용됨)에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM 절에 영향을 줌
- 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵다는 단점이 있음. 단순하고 성능에 이슈가 없으면 크게 문제가 안 되지만 성능이 중요하면 분석하기 쉽도록 묵시적 조인보다는 명시적 조인을 사용하자
서브 쿼리
- 서브 쿼리를 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용할 수 없음
서브 쿼리 함수
- [NOT] EXISTS (subquery)
- {ALL | ANY | SOME} (subquery)
- [NOT] IN(subquery)
Functions
Operation
- 비교 연산 : =, >,
>=
, <,<=
, <>(다름)
객체지향 쿼리 심화
벌크 연산
- 주의점 : 벌크 연산을 사용할 때는 벌크 연산이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점에 주의해야 함 → 벌크연산 하고나서 영속성 컨텍스트 초기화! ⇒ @Modifying(clearAutomatically=true)
public interface PostRepository extends JpaRepository<Post,Long> { @Modifying @Query("UPDATE Post p SET p.title = :title WHERE p.id = :id") int updateTitle(String title, Long id); }
JPQL로 조회한 엔티티와 영속성 컨텍스트
- 영속성 컨텍스트에 있는 엔티티라면 JPQL로 db에서 가져온 데이터를 버리고 대신 영속성 컨텍스트에 있던 엔티티를 반환함
- 영속성 컨텍스트는 영속 상태인 엔티티의 동일성을 보장해야 하므로 em.find()로 조회하든 JPQL로 조회하든 영속성 컨텍스트가 같으면 동일한 엔티티를 반환함
find() vs JPQL
- em.find() 는 엔티티를 영속성 컨텍스트에서 먼저 찾고(1차 캐쉬) 없으면 db를 찾음
- JPQL은 항상 데이터베이스에 SQL을 실행해서 결과를 조회함
JPQL의 특징
- JPQL은 항상 데이터베이스를 조회함
- JPQL로 조회한 엔티티는 영속 상태임
- 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환함
성능 개선
JPA exists 쿼리 성능 개선
[JPA exists 쿼리 성능 개선 — jojoldu] → 결론은 queryDsl exists도 내부적으로는 count 쿼리 활용, limit 1 으로 exists와 동일한 성능 낼수 있다!
- JPQL에서는 select 의 exists를 지원하지 않음
- 우회하는 방식으로 count 쿼리 이용하는데
- 실제 raw 쿼리로 날려보면 count 쿼리는 전체 데이터를 다 확인해야 해서 exist에 비해 성능이 느릴 수 밖에 없음
- exist는 존재하는 데이터가 있다면 바로 return 하게 되고
- exists가 count보다 성능이 좋은 이유가 결국 전체를 조회하지 않고 첫번째 결과만 확인하기 때문입니다.
JPQL에서 limit 을 사용하는 방법
- 쿼리메소드로 정의
boolean
existsByNameIn(
List
<String> name);
- 쿼리메소드의 Top, First와 같은 정의 사용 - Spring Data, Limit Query Results
User findFirstByOrderByLastnameAsc();
- Pageable 사용하기 - Spring Data, Special Parameter Handling
@Query("SELECT s FROM Students s ORDER BY s.id DESC") List<Students> getLastStudentDetails(Pageable pageable); getLastStudentDetails(PageRequest.of(0,1));