HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🤩
개발
/
Spring Data
Spring Data
/
🧣
JPA(Java Persistence API)
/
📍
JPQL(Java Persistence Query Language)
📍

JPQL(Java Persistence Query Language)

  • 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'
여기서 m.username, m.team, m.orders, t.name이 모두 경로 표현식을 사용한 예임
  • 상태 필드 경로: 경로 탐색의 끝. 더 탐색할 수 없음
  • 단일 값 연관 경로(연관관계) : 묵시적으로 내부 조인이 일어남. 단일 값 연관관계는 계속 탐색할 수 있음
  • 컬렉션 값 연관 경로: 묵시적으로 내부 조인이 일어남. 더는 탐색할 수 없다. 단 FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있음
경로 탐색을 사용한 묵시적 조인 시 주의사항
  • 항상 내부 조인임
  • 컬렉션은 경로 탐색의 끝. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 함
  • 경로 탐색은 주로 SELECT, WHERE절(다른 곳도 사용됨)에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM 절에 영향을 줌
  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵다는 단점이 있음. 단순하고 성능에 이슈가 없으면 크게 문제가 안 되지만 성능이 중요하면 분석하기 쉽도록 묵시적 조인보다는 명시적 조인을 사용하자
서브 쿼리
  • 서브 쿼리를 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용할 수 없음
서브 쿼리 함수
  • [NOT] EXISTS (subquery)
  • {ALL | ANY | SOME} (subquery)
  • [NOT] IN(subquery)

Functions

https://en.wikibooks.org/wiki/Java_Persistence/JPQL#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 을 사용하는 방법

  1. 쿼리메소드로 정의
    1. boolean existsByNameIn(List<String> name);
  1. 쿼리메소드의 Top, First와 같은 정의 사용 - Spring Data, Limit Query Results
    1. User findFirstByOrderByLastnameAsc();
  1. Pageable 사용하기 - Spring Data, Special Parameter Handling
    1. @Query("SELECT s FROM Students s ORDER BY s.id DESC") List<Students> getLastStudentDetails(Pageable pageable); getLastStudentDetails(PageRequest.of(0,1));