Back-End/JPA

JPA - JPQL

Meluu_ 2024. 7. 12. 15:38

JPA는 SQL을 추상화한 JPQL은 객체 지향 쿼리 언어를 제공

JPQL은 테이블이 아닌 엔티티 객체를 대상

특정 데이터베이스 SQL에 의존 X

즉, 객체 지향 SQL 

 

 

참고 

JPA를 사용하면서 JDBC 직접 사용 Template 등을 사용하게 되면

SQL을 실행하기 전에 영속성 컨텍스트를 수동 플러시 하자

 

 

 

✔️ 문법


 

select m from Member (as) m where m.age > 18

엔티티와 속성은 대소문자 구분 O (Member, age 등)

JPQL 키워드는 대소문자 구분 X (SELECT, select 등)

엔티티 이름 사용

 

💠 집합과 정렬

select
 COUNT(m), //회원수
 SUM(m.age), //나이 합
 AVG(m.age), //평균 나이
 MAX(m.age), //최대 나이
 MIN(m.age) //최소 나이
from Member m

 

 

GROUP BY, HAVING, ORDER BY 사용가능

 

 

💠 TypeQuery, Query

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

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

 

TypedQuery<Member> query =
 em.createQuery("SELECT m FROM Member m", Member.class); // Member.class 를 적어둬서 타입을 명확하게 가리킴
 
Query query =
 em.createQuery("SELECT m.username, m.age from Member m");

 

 

결과 조회 API

query.getResultList() // 결과가 하나 이상 , 리스트 반환 , 없으면 빈 리스트 반환
query.getSingleResult() // 결과가 정확히 하나, 단일 객체 반환 
// 없으면 jakarta.persistence.NoResultException
// 둘이상이면 jakarta.persistence.NonUniqueResultException

 

 

파라미터 바인딩

"SELECT m FROM Member m where m.username=:username" // 컬럼=:파라미터명
query.setParameter("username", usernameParam);

 

 

프로젝션

SELECT 절에 조회 대상을 지정하는 것

SELECT m FROM Member m // 엔티티 프로젝션
SELECT m.team FROM Member m  // 엔티티 프로젝션
SELECT m.address FROM Member m // 임베디드 타입 프로젝션
SELECT m.username, m.age FROM Member m // 스칼라 타입 프로젝션

 

 

 

✔️ 여러 값 조회


1. Query 타입으로 조회

 아래와 유사함

 

2. Object[] 타입으로 조회

Member member = new Member();
member.setUsername("1번학생");
member.setAge(12);

Member member1 = new Member();
member1.setUsername("2번학생");
member1.setAge(22);

em.persist(member);
em.persist(member1);

em.flush();
em.clear();


List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m")
        .getResultList();

for (Object[] objects : resultList) {
    for (Object info : objects) {
        System.out.print(info + " ");
    }
    System.out.println();
}

 

3. new 명령어로 조회

  • 단순 값을 DTO로 바로 조회
  • SELEECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
  • 패키지명을 포함한 전체 클래스 명 입력
  • 순서와 타입이 일치하는 생성자 필요
package jpql.jpql;

public class MemberDTO {

    private String username;
    private int age;

    public MemberDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }
 	
    // getter , setter ,toString 메소드들..
}
List<MemberDTO> resultList = em.createQuery("select new jpql.jpql.MemberDTO(m.username, m.age) from Member m")
		.getResultList();


for (MemberDTO memberDTO : resultList) {
    System.out.println("memberDTO = " + memberDTO);
}

 

 

 

 

✔️ 페이징


setFristResult(int start Position) : 조회 시작 위치 (0부터 시작)

 

setMaxResults(int maxResult) : 조회할 데이터 수 

 

List<Member> resultList = em.createQuery("select m from Member m", Member.class)
        .setFirstResult(0) // 0부터
        .setMaxResults(1) // 1명
        .getResultList();


for (Member member2 : resultList) {
    System.out.println("member2 = " + member2);
}

2명의 학생중 1명만 가져옴

 

 

 

 

✔️ 조인


// 내부 조인
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. 조인 대상 필터링
// 2. 연관관계 없는 엔티티 외부 조인
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'

// SQL에서는 
SQL:
SELECT m.*, t.* FROM
Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'

 

서브 쿼리도 가능

하이버네이트 6부터 FROM도 가능

 

 

 

✔️ 조건식 


CASE식 

 

COALESCE: 하나씩 조회해서 null이 아니면 반환

select coalesce(m.username, '이름이 없는 회원') from Member m

NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환

select NULLIF(m.username, '관리자') from Member m

 

  • CONCAT
  • SUBSTRING
  • TRIM
  • LOWER, UPPER
  • LENGTH
  • LOCATE
  • ABS
  • SQRT
  • MOD 
  • SIZE, INDEX(JPA 용도)

 

사용자 정의 함수도 가능

이는 하이버네이트가 버전 업데이트 후 바뀌어서 직접 찾아보고 바꿔야함

자세한 건 필요할때 찾아서 사용하자

 

 

✔️ 경로 표현식


.(점)을 찍어 객체 그래프를 탐색

select m.username -> 상태 필드
 from Member m
 join m.team t -> 단일 값 연관 필드
 join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'

 

상태 필드 : 단순 값 저장 필드 , 경로 탐색의 끝, 탐색 x

연관 필드 : 연관관계를 위한 필드

  • 단일 값 : 묵시적 내부 조인 발생, 탐색 o
  • 컬렉션 값 : 묵시적 내부 조인 발생, 탐색 x
    • FROM 절에서 명시적 조인을 통해 탐색 가능
// 단일 값 연관 필드         하나의 객체 엔티티
@ManyToOne, OneToOne, 대상이 엔티티 (m.team)
// 컬렉션 값 연관 필드      여러개 객체 컬렉션 엔티티
@OneToMany, @ManyToMany, 대상이 컬렉션 (m.orders)

 

// 단일 값 연관 경로 탐색
select o.members from Order o

// 묵시적 내부조인
SQL:
select m.*
 from Orders o
 inner join Member m on o.member_id = m.id

 

 

💠 명시적 조인 

select m from Member m join m.team t // join 키워드를 통해 team을 명시
select t.members.username from Team t -> 실패
select m.username from Team t join t.members m -> 성공

 

컬렉션은 조회하면 타입이 컬렉션 타입으로 되기에 탐색이 불가능 

따라서 명시적 조인이 필요

 

 

결론

직접 쿼리를 날릴 경우

무 조 건 명시적 조인 사용, 내부적 조인 사용 금지

 

 

 

 

✔️ 페치 조인


SQL 조인 종류 x

JPQL에서 성능 최적화를 위해 제공하는 기능

연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회 가능

select m from Member m join fetch m.team

 

 

참고

하이버네이트 6 부터 DISTINCT를 쓰지 않아도 자동으로 엔티티의 중복을 제거

 

 

일반 조인과 페치조인 차이점

일반 조인은 연관된 엔티티를 함께 조회 X

그저 select 에 지정한 엔티티만 조회

 

페치 조인은 연관된 엔티티도 함께 조회 (즉시 로딩)

페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념

 

[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'

 

 

페치조인의 특징과 한계

페치 조인 대상에는 별칭을 줄 수 없다. (하이버네이트는 가능하지만 쓰지말자)

둘 이상의 컬렉션은 페치 조인 불가능

컬렉션을 페치 조인하면 페이징 API 사용 불가

  • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징 (위험)

 

엔티티와 직접 적용하는 글로벌 로딩 전략보다 우선함

@OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략

최적화가 필요한 곳에 사용

 

 

단, 여러 테이블을 조인한 다음 엔티티 그 자체의 값이 아닌 특정 값만 골라서 결과를 내야한다면
페치 조인보다는 일반 조인 후  필요한 데이터들만 조회해서 DTO로 반환하는게 더 효과적

 

 

 


🌱 컬렉션을 페치조인한 것처럼 하고 페이징 하기 

컬렉션 페치 조인 대신

컬렉션에 @Batch_size()를 붙이고, Team을 조회하고  member에 각각 접근하면  

where에 in 으로 조회한 Team과 연관된 모든 멤버를 다 가져온다.

이를 통해  N+1 문제가 해결된다. 

 

@BatchSize(size = 100) // in 쿼리 ? 100개를 날리겠다는 의미
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
/**
* 회원 1과 회원 2는 TeamA
* member 3은 teamB 로 엔티티를 저장한 상황
*/

List<Team> result = em.createQuery("select t from Team t ", Team.class)
                    .setFirstResult(0)
                    .setMaxResults(100)
                    .getResultList();

 

 


💠다형성 쿼리

조회 대상을 특정 자식으로 한정

상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용

FROM, WHERE, SELECT(하이버네이트 지원) 사용

[JPQL]
select i from Item i
where type(i) IN (Book, Movie)

[SQL]
select i from i
where i.DTYPE in (‘B’, ‘M’)

 

[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 에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용

[JPQL]
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용

[SQL](JPQL 둘다 같은 다음 SQL 실행)
select count(m.id) as cnt from Member m

 

 

 

✔️ Named 쿼리


미리 이름과 정적 쿼리를 정의해두고 사용 (XML , 애노테이션 정의)

애플리케이션 로딩 시점에  쿼리를 검증 및 초기화 후 재사용

 

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


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

 

 

 

스프링 데이터 JPA에서 인터페이스로 메서드 이름만 혹은 쿼리를 작성하여 사용(Named 쿼리)

 

✔️ 벌크 연산


변경된 데이터가 100개라 했을 때 100번의 update 쿼리를 날릴 순 없다.

벌크 연산을 통해 쿼리 한번으로 여러 테이블 로우를 변경(엔티티)

UPDATE, DELETE 지원

INSERT(insert into.. select, 하이버네이트 지원)

int resultCount = em.createQuery("update Product p set p.price = p.price * 2 where p.price < :price")
 .setParameter("price", 1000)
 .executeUpdate(); // 영향받은 엔티티 수 반환

 

 

주의 사항

벌크 연산은 영속성 컨텍스트무시하고 데이터베이스에 직접 쿼리

 

벌크 연산을 먼저 실행 후 영속성 컨텍스트 초기화

 

 

벌크 연산으로 엔티티의 특정 값들을 변경하고

find 해도 영속성 컨텍스트는 그대로이기 때문에 캐시에 있는 변경 전의 값을 그대로 반환한다. 

따라서 em.clear() 한 뒤 find 해야 db에서 다시 가져온다. 

 

🔖 학습내용 출처


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

김영한. (2021). 자바 ORM 표준 JPA 프로그래밍. 에이콘출판사

'Back-End > JPA' 카테고리의 다른 글

스프링 데이터 JPA - 기본  (0) 2024.07.29
API 학습 내용  (0) 2024.07.15
JPA - 값타입  (0) 2024.07.12
JPA - 프록시와 연관관계 관리  (0) 2024.07.10
JPA - 고급 매핑  (0) 2024.07.09