[JPA] JPQL(객체지향 쿼리 언어) 정리

2025. 7. 1. 15:51·DB Access/JPA

 

객체지향 쿼리 언어

#JPA/기본


/JPA가 지원하는 다양한 쿼리 방법

JPQL (아래에서 자세히 다룸)


JPA Criteria

  • /Criteria 소개: 문자열 기반 jpql의 한계를 극복하기 위해 등장.
  • 너무 복잡하고 실용성이 없음.

QueryDSL

  • /QueryDSL 소개: JPQL 빌더 역할
  • 자바 코드로 JPQL 작성할 수 있음.
  • 컴파일 시점 문법 오류 찾을 수 있고, 동적 쿼리 작성이 편리함.
  • 단순, 쉬움, 실무 사용 권장.

네이티브 SQL

  • /네이티브 SQL 소개
  • em.createNativeQuery(sql, Member.class)
  • SQL을 직접 사용. JPQL로 해결할 수 없는 특정 DB에 의존적인 기능을 위해 사용
  • JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용
    • /JDBC 직접 사용, SpringJdbcTemplate 등

/JPQL(Java Persistence Query Language)

  • 객체지향 쿼리 언어
  • 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리
  • 특정 DB에 의존 X

/JPQL 문법

  • select m from Member as m where m.age > 18
  • 엔티티와 속성은 대소문자 구분O (Member, age)
  • JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)
  • 엔티티 이름 사용, 테이블 이름이 아님(Member)
  • 별칭은 필수(m) (as는 생략가능)

/집합과 정렬

  • GROUP BY, HAVING, ORDER BY 지원

/TypeQuery, Query

TypeQuery: 반환 타입이 명확할 때 사용

  • TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
  • TypedQuery<String> query1 = em.createQuery("select m.username, m.age from Member m", String.class);
  • Query: 반환 타입이 명확하지 않을 때 사용
    • Query query = em.createQuery("SELECT m.username, m.age from Member m");

/결과 조회 API

  • query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
    • 결과가 없으면 빈 리스트 반환.
  • query.getSingleResult(): 결과가 정확히 하나일때만 사용, 단일 객체 반환
    • 결과가 없으면: javax.persistence.NoResultException
    • 둘 이상이면: javax.persistence.NonUniqueResultException
    • TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
    • Member result = query.getSingleResult(); // 예외 발생
  • Spring Data JPA 결과가 없으면 Optional로 반환해버린다. (스프링 내부에서 try-catch로 처리해줌)

/파라미터 바인딩 - 이름 기준, 위치 기준

Member result = em.createQuery("select m from Member m where m.username = :username", Member.class)
        .setParameter("username", "member1")
        .getSingleResult()
  • 이름 기준 방식을 사용하자.
  • 위치 기준 방식은 사용하지 말자.

/프로젝션: select 대상이 엔티티? 임베디드? 스칼라? SELECT 절에 조회 대상을 지정하는 것.

/엔티티 프로젝션: SELECT절의 결과가 10개든 20개든 전부 영속성 컨텍스트에서 관리된다.

  • SELECT m FROM Member m, m은 엔티티의 별칭
  • SELECT m.team FROM Member m, 여기서 team도 엔티티
    • JPA 내부에서 Member와 Team을 조인을 해서 team을 가져온다.
    • 조인이 명시적으로 드러나지 않는다. 예측하기 어려운 이런 방식은 좋은 방식이 아니다.
    • 다음과 같이 사용하자: List<Team> teams = em.createQuery("select t from Member m join m.team t", Team.class).getResultList(); (명시적으로 조인을 드러냄)
  • 조회한 엔티티는 더티체킹이 된다.

/임베디드 타입 프로젝션: 속한 엔티티로부터 쿼리를 시작해야 한다.

  • SELECT m.address FROM Member m
  • em.createQuery("select a from Address a", Address.class).getResultList(); 이런 건 안 된다. 엔티티로부터 쿼리가 시작해야 한다.

/프로젝션 - 여러 값 조회 (스칼라 프로젝션): 그냥 데이터 가져오는 것

  • em.createQuery("select distinct m.username, m.age from Member m").getResultList();
  • 필드가 여러 개 인데 값을 각각 어떻게 받아야 할까?
  1. Query 타입 조회 후 캐스팅(List resultList Object 1개 Object 캐스팅)
  2. Object 타입으로 조회 (1이랑 사실상 거의 같음) (List<Object> Object 1개)
  3. new 명령어로 조회 - DTO에다가 담음. (생성자)
    • List<MemberDTO> resultList = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class).getResultList();

/페이징 API

  • setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
  • List<Member> resultList = em.createQuery(jpql, Member.class)<br> .setFirstResult(10)<br> .setMaxResults(20)<br> .getResultList();
  • 페이징의 경우 DB마다 방언이 제 각각인데, H2는 방언을 사용할 DB를 지정해주면 알아서 쿼리를 에뮬레이팅 해준다.
    • hiberante.dialect
    • MySQL LIMIT, Oracle ROWNUM
  • 스프링 Data JPA를 쓰면 페이징을 더 쉽게 다룰 수 있따.

/조인: .으로 연관된 엔티티를 표현(객체니까)

  • 내부, 외부, 세타 조인 모두 가능
    • SELECT m FROM Member m [INNER] JOIN m.team t
    • SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
    • select count(m) from Member m, Team t where m.username = t.name

/조인 - ON 절

  • /1. 조인 대상 필터링
    • ex) 팀 이름이 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 and t.name='A'
  • /2. 연관관계 없는 엔티티 외부 조인(팀과 멤버가 연관관계가 없는 상황이라고 해보자)
    • 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
  • /서브 쿼리
    • ex) select m from Member m where m.age > (select avg(m2.age) from Member m2)
      • 상위에서 만든 엔티티 별칭을 서브 쿼리로 가져오지 않고 따로 써야 성능이 잘 나온다.
    • /서브 쿼리 지원 함수
      • NOT, EXISTS, ALL, ANY, SOME, IN 지원한다.
    • /JPA 서브 쿼리 한계
      • FROM 절의 서브 쿼리가 하이버네이트6부터 JPQL에서 지원됨
        • 원래는 안됬어서 조인으로 풀 수 있으면 풀어서 해결하거나 Application에서 조작했다.

/JPQL 타입 표현

  • 문자: ‘HELLO’, ‘She’’s’
  • 숫자: 10L(Long), 10D(Double), 10F(Float)
  • Boolean: TRUE, FALSE
  • ENUM: jpabook.MemberType.Admin (패키지명 포함)
    • select m.username, 'HELLO', true from Member m " + "where m.type = jpql.MemberType.ADMIN
    • ENUM의 경우 파라미터 바인딩을 해주면 패키지명을 안써도 되서 편해진다.
      • em.createQuery(query).setParameter("userType", MemberType.ADMIN).getResultList();
  • 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)
    • ex) Item 중 Book인 것 조회
    • em.createQuery("select i from Item i where type(i) = Book", Item.class)...
      • Item 레코드 중 Dtype이 @DiscriminatorValue의 name인걸 골라준다.

/JPQL 기타: EXISTS, IN, AND, OR, NOT, =, <=, BEWEEN, LIKE, IS NULL 다 지원한다.

  • /조건식 - CASE 식: CASE WHEN THEN ELSE END 지원한다.
  • COALESCE: 하나씩 조회해서 null이 아니면 반환
  • NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
    • 사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환
    • select NULLIF(m.username, '관리자') from Member m

/JPQL 기본 함수

  • CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE
  • ABS, SQRT, MOD, SIZE, INDEX

/사용자 정의 함수 호출

  • 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록하여 사용 가능하다.
  • /Hibernate6에서 함수 등록

/JPQL - 경로 표현식

  • /경로 표현식: .찍어서 객체 그래프 탐색하는 걸 말함.
  • 상태 필드(ex. m.username)
    • 경로 탐색의 끝. 더이상 탐색 X
  • 연관 필드
    • 단일 값 연관 필드(ex. m.team)
      • 묵시적 내부 조인 발생, 탐색 O(더 갈 수 있음. m.team.name)
      • 실무에서는 묵시적 내부 조인 사용을 자제하자.
        • 직접 join 키워드를 사용하자. (from Member m join m.team t)
      • JPQL과 SQL을 비슷하게 맞추자.
    • 컬렉션 값 연관 필드(ex. m.orders)
      • String query = "select t.members from Team t";
      • 묵시적 내부 조인 발생, 탐색 X
      • t.members도 컬렉션인데, 여기서 추가적으로 t.members.name 이러면 어떤 member의 Name을 가져올 지 애매해진다.
      • 여기서 더 이상 탐색이 불가능하다. (size는 가능)
      • 그런데 t.members.name 해가지고 이름 목록 같은 걸 가지고 오고 싶을 수 있다.
        • FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색이 가능하다.
        • String query = "select m from Team t join t.members m";
  • 결론: 묵시적 조인 쓰지 말자. 명시적 조인 쓰는게 튜닝하기도 좋음.
    • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 줌
    • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

/JPQL - 페치 조인(fetch join)

/페치 조인(fetch join)

  • JPQL에서 성능 최적화를 위해 제공하는 JPQL 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능

/엔티티 페치 조인

  • 페치 조인을 안쓰면 연관관계 대상 N개를 가져올 때, 최대 N번의 쿼리가 추가로 나가 총 N + 1번의 쿼리가 나갈 수 있다. (N+1 문제)
  • 페치 조인을 사용하면 한 방에 가져온다.
    • ex) 회원을 조회하면서 연관된 팀도 함께 조회(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
    • resultList에 담기는 시점에 team은 프록시가 아니라 실제 데이터가 담긴 거다.
    • select t from Team t join fetch t.members where t.name = ‘팀A'

/페치 조인과 DISTINCT

  • 하이버네이트 6 오면서 크게 신경쓸 필요는 없는 내용

/페치 조인과 일반 조인의 차이

  • 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음 N+1 문제가 발생해서 쿼리가 많이 나가게 됨.
  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

/페치 조인의 특징과 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
    • 별칭 주고 그래프 탐색 사용하는 순간 페치 조인의 목적 자체가 깨져버림
      • fetch join은 목적이 나랑 연관된 얘들을 전부 끌고오는 거다.
      • 그런데 성능상 몇 개만 일치하는 것만 가져오고 싶어서 별칭을 준다고 해보자.
      • 예를 들어 팀과 연관된 회원이 5명인데, 그 중에 1명만 불러오게 하면, 나머지 4명이 누락되게 된다. 페치 조인 목적이랑 전혀 맞지 않다.
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
    • 아까 팀이랑 멤버 조인한 테이블에서 팀A에 대한 레코드가 두 개 생겼다.(회원이 2명이라)
      • 여기서 만약 페이징으로 팀을 1개만 조회한다면? 회원1만 있다고 생각하고, 회원 2는 누락되버린다.
      • 물론 하이버네이트 6에서 팀을 1개만 있다고 인식하긴 하지만, 경고가 나오는 것 같다.
    • 물론 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
    • 해결 방법1: 다대일로 풀어낸다 (member로 부터 조회한다)
    • 해결 방법2: 죽어도 일대다로 풀어야 한다면, @Batch 이용
      • /@Batch로 풀어내는 방법
    • 해결 방법3: DTO 사용
  • /페치 조인의 특징과 한계 - 전략
    • 지연 로딩 + 최적화가 필요한 경우에 페치 조인을 적용하자.
  • /페치 조인 - 정리
    • 모든 것을 페치 조인으로 해결할 수 는 없음
    • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
    • 모양이 엔티티와 많이 달라지면 DTO로 반환해서 쓰자.

/JPQL - 다형성 쿼리

  • /TYPE: 조회 대상을 특정 자식으로 한정
    • select i from Item i where type(i) IN (Book, Movie)
  • /TREAT(JPA 2.1): 타입 캐스팅과 유사
    • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
    • JPQL: select i from Item i where treat(i as Book).author = ‘kim’ (싱글 테이블)
    • SQL: select i.* from Item i where i.DTYPE = ‘B’ and i.author = ‘kim’

/JPQL - 엔티티 직접 사용: JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 PK 값으로 쿼리를 날린다.

  • /엔티티 직접 사용 - 기본 키 값: 엔티티 직접 사용 시 pk 값을 사용한다.
    • where m = :member SQL에선 member의 PK 쓴다.
  • /엔티티 직접 사용 - 외래 키 값: 연관관계 걸린 엔티티 사용하면 JoinColumn에서 지정한 FK 칼럼이 적용됨.

/JPQL - Named 쿼리

  • /Named 쿼리 - 정적 쿼리: 미리 정의해서 이름을 부여해두고 사용하는 JPQL. 동적 쿼리는 불가
    • Annotation or XML에 정의
    • Application 로딩시점에 초기화 후 재사용 로딩 시점에 쿼리 검증
  • /Named 쿼리 - 어노테이션

@Entity
@NamedQuery(
        name = "Member.findByUsername",
        query="select m from Member m where m.username = :username")
public class Member {
    ...
}

name의 관례는 {entitiy명}.{쿼리 이름} 이다.


List<Member> resultList = 
  em.createNamedQuery("Member.findByUsername", Member.class)
        .setParameter("username", "회원1")
        .getResultList();

  • /Named 쿼리 - XML에 정의
  • /Named 쿼리 환경에 따른 설정: XML이 항상 우선권을 가진다. 운영 환경마다 다른 XML 배포가 가능하다.
    • 스프링 Data JPA에서는 Named 쿼리 대신 DAO 인터페이스에 @Query(쿼리) 붙여서 쓴다.

/JPQL - 벌크 연산

  • /벌크 연산: 쿼리 한 번으로 여러 테이블의 row를 변경한다.
  • /벌크 연산 예제
    • executeUpdate()의 결과는 영향받은 엔티티 수 반환
    • em.createQuery(qlString).executeUpdate();
  • /벌크 연산 주의: 영속성 컨텍스트를 무시하고 DB에 직접 쿼리
    • 방법1: 벌크 연산을 먼저 실행 (영속성 컨텍스트에 아무것도 없는 경우)
    • 방법2: 벌크 연산 수행 후 영속성 컨텍스트 초기화 (영속성 컨텍스트에 뭔가 있는 경우)

예를 들어 영속성 컨텍스트에 회원 연봉이 원래 5천만원이었는데 벌크를 날려서 6천만원으로 변경되었다. 이러면 DB에는 6천만원인데 Application에는 5천만원으로 남아있게 된다. (벌크 연산은 영속성 컨텍스트를 거치지 않고 DB를 수정하므로) 그러기 때문에 영속성 컨텍스트를 초기화해줘야 한다.


Ref) 김영한 자바 ORM 표준 JPA 프로그래밍 - 기본편

'DB Access > JPA' 카테고리의 다른 글

[JPA] 값 타입  (0) 2025.07.01
[JPA] 프록시와 연관관계 관리  (0) 2025.07.01
[JPA] 고급 매핑 - 상속 관계 매핑  (0) 2025.07.01
[JPA] 다양한 연관관계 매핑  (0) 2025.07.01
[JPA] 연관관계 매핑 기초  (0) 2025.07.01
'DB Access/JPA' 카테고리의 다른 글
  • [JPA] 값 타입
  • [JPA] 프록시와 연관관계 관리
  • [JPA] 고급 매핑 - 상속 관계 매핑
  • [JPA] 다양한 연관관계 매핑
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (463)
      • 개발 일지 (0)
        • Performance (0)
        • TroubleShooting (0)
        • Refactoring (0)
        • Code Style, Convetion (0)
        • Architecture (0)
      • Software Engineering (36)
        • Test (8)
        • 이론 (18)
        • Clean Code (10)
      • Java (72)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (13)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (16)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (130)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (6)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (77)
        • Kotlin (1)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[JPA] JPQL(객체지향 쿼리 언어) 정리
상단으로

티스토리툴바