ORM 표준 JPA (4)

이정원·2025년 2월 17일
post-thumbnail

1.JPQL 개요

  • JPQL은 객체지향 쿼리 언어다.테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.

  • SQL을 추상화하여 특정 DB SQL에 의존하지 않는다.

  • JPQL은 결국 SQL로 번역된다.

1-1.JPQL 문법

SELECT m from Member as m where m.age > 18

1.엔티티(Member)와 속성(age)는 대소문자를 구분한다.
2.키워드(SELECT,FROM,where)은 대소문자를 구분하지 않는다.
3.엔티티 이름을 사용한다.(테이블x)
4.별칭은 필수(m) (as는 생략가능)

집계 함수

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: 반환 타입이 명확할때
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
  • Query: 반환 타입이 명확하지 않을때
Query query = em.createQuery("SELECT m.username, m.age from Member m"); 

결과 조회 API

  • query.getResultList(): 결과가 하나 이상일 때(결과가 없으면 빈 리스트 반환)
  • query.getSingleResult(): 결과가 정확히 하나(결과가 없으면 NoResultException, 둘 이상이면 NonUniqueResultException)

1-2.프로젝션

SELECT 절을 사용하여 원하는 특정 데이터(필드)만 선택해서 조회하는 것

(1) 전체 엔티티 프로젝션

SELECT m FROM Member m

(2) 임베디드 타입 프로젝션

SELECT m.address FROM Member m

(3) 스칼라 타입(특정 필드) 프로젝션

SELECT m.username, m.age FROM Member m

프로젝션 대상(결과)들은 영속성 컨텍스트에 의해 관리된다.

List<Member> result=em.createQuery("select m from Member m",Member.class).getResultList();

Member findMember=result.get(0);
findMember.setAge(20); // -> 실제 DB 데이터는 변경된다.

2.페이징

페이징(Paging)이란 대량의 데이터를 한 번에 가져오지 않고 일정한 크기로 나누어 가져오는 기법이다.

  • setFirstResult(int startPosition):조회 시작 위치
  • setMaxResults(int maxResult): 조회할 데이터 수
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
                    .setFirstResult(1)
                    .setMaxResults(10)
                    .getResultList();

2-1.조인

조인(JOIN)은 두 개 이상의 테이블을 연결하여 관련된 데이터를 하나의 결과로 반환하는 SQL 연산이다.

1.내부 조인: 테이블에서 공통된 데이터(일치하는 행)만 반환한다.
2.외부 조인: 한쪽의 테이블은 모두 포함하고 일치하는 오른쪽 데이터를 반환한다.(없을시 NULL)
3.세타 조인: 관계가 없는 두 테이블을 cartesian 곱과 조건절로 반환한다.

ON 절: 조인할 두 테이블 간의 연관 관계를 명확하게 지정할 때 필요하다. 또한 연관관계가 없는 테이블에서도 ON을 통한 조인이 가능하다. 실제 SQL문에서 AND 조건으로 나타난다.

String query="select m from Member m left join m.team t on t.name='teamA'";

String query="select m from Member m left join Team t on m.username=t.name"

위 예시에서 첫번째 조인은 엔티티에 연관 필드인 Team 엔티티를 조인하고, 두번째 조인은 Team 테이블을 직접 조인하는 다양한 ON절을 활용할수 있다.

3.서브 쿼리

서브 쿼리(Sub Query)란 특정 쿼리로 인한 결과 값이 쿼리 문의 파라미터로 필요할때 사용한다.

서브 쿼리 함수
1.EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
2.ALL: 주어진 조건이 서브쿼리의 모든 결과에 참인지 확인
3.ANY,SOME: 조건을 하나라도 만족하면 참
4.IN: 특정 값이 지정된 리스트나 서브쿼리의 결과에 포함되는지를 확인하는 비교 연산자

JPA 서브 쿼리 한계

  • JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능
  • SELECT 절도 가능(하이버네이트에서 지원)
  • FROM 절의 서브 쿼리는 현재 JPQL에서 불가능
  • 조인으로 풀 수 있으면 풀어서 해결

JPQL 타입 표현

  • 문자: 'HELLO','She''s'
  • 숫자: 10L(Long),10D(Double),10F(Float)
  • Boolean: TRUE,FALSE
  • ENUM: 패키지명 포함
  • 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)
String query="select m.username,HELLO,true From Member m"+
				"where m.type = jpashop.jpql.MemberType.ADMIN";

조건식

기본 CASE 식

select 
	case when m.age <= 10 then '학생요금' 
		 when m.age >= 60 then '경로요금' 
		 else '일반요금'
    end
from Member m

단순 CASE 식

select 
	case t.name 
		when '팀A' then '인센티브110%'
 		when '팀B' then '인센티브120%'
 		else '인센티브105%' 
	end 
from Team t
  • COALESCE: 하나씩 조회해서 null이 아니면 반환
String query="select coalesce(m.username,'이름 없는 회원')"+"from Member m";
  • NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
String query="select nullif(m.username,'관리자')"+"from Member m";

3-1.JPQL 기본 함수

  • CONCAT
  • SUBSTRING
  • TRIM
  • LOWER,UPPER
  • LENGTH
  • LOCATE
  • ABS,SQRT,MOD
  • SIZE,INDEX
  • 사용자 정의 함수

4.경로 표현식

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

select m.username -> 상태 필드 
	from Member m  
     join m.team t    -> 단일 값 연관 필드 
     join m.orders o -> 컬렉션 값 연관 필드 
where t.name = '팀A'
  • 상태 필드: 경로 탐색의 끝,탐색 X
  • 단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생,탐색 O
    (객체 입장에선 참조로 바로 조회가 가능하지만 DB에서 join이 필요하다.)
  • 컬렉션 값 연관 경로: 묵시적 내부 조인 발생
String query="select t.members From Team t";

위 코드에서 컬렉션에 들어있는 여러 컬렉션에서 어떤 필드를 가져와야 하는지 모호하고 결과 값은 List가 반환되고 필드는 탐색하지 않는다. 다음과 같이 From 절에서 명시적 join으로 별칭을 얻어 컬렉션 필드에 대한 조회가 가능하다.

String query="select m From Team t join t.members m";

실무 조언

  • 가급적 명시적 조인(join)을 사용
  • 조인은 SQL 튜닝의 중요 포인트
  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

5.페치 조인(fetch join)

페치 조인은 연관된 엔티티들을 한번의 SQL 쿼리로 조회할수 있게 한다. 기본적으로 연관관계에 있는 엔티티들은 지연 로딩(Lazy Loading)으로 설정하고 접근하면 N+1 문제가 발생하지만 페치 조인은 JPQL을 통해 설정 상관없이 명시적으로 동적인 타이밍에 즉시 로딩(Eager Loading)으로 불러온다.

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

예시

위와 같이 Member와 Team 엔티티가 있고 연관관계 외래키가 Team_id로 이루어져 있다.

fetch 조인 결과

페치 조인을 수행하면 한번의 쿼리로 위와 같이 Inner join이 수행된 결과 테이블을 반환한다.

영속성 컨텍스트

이후 영속성 컨텍스트엔 위와 같은 그림으로 저장된다.

5-1.@ManyToOne 페치 조인

String query="select m From Member m";

List<Member> result = em.createQuery(query, Member.class)
                    .getResultList();

for (Member m:result){
               System.out.println("member= " +m.getUsername()+", "+m.getTeam().getName());
}

위와 같이 member에 대한 쿼리만 진행하고 getTeam으로 연관관계 필드에 접근하면 지연 로딩 설정으로 인해 쿼리 결과 개수인 N+1의 쿼리가 더 진행된다. 해당 문제를 해결하기 위해 다음과 같은 페치 조인을 수행한다.

String query="select m From Member m join fetch m.team";

연관 컬렉션 필드를 지연 로딩으로 설정했어도 쿼리문에서 fetch join을 하면 즉시 로딩이 우선이다.

5-2.@OneToMany 페치 조인

One쪽의 컬렉션을 페치 조인 하면 하나의 엔티티가 해당 컬렉션의 요소 수만큼 중복되어 저장된다. 이 경우, JPA가 자동으로 중복을 제거하여 결과를 반환한다.

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'

5-3.일반 조인(inner join)과 페치 조인의 차이

일반 조인(inner join)
1.Fetch Join과 달리 연관 Entity에 Join을 걸어도 실제 쿼리에서 SELECT 하는 Entity는 조회 되는 Entity만 조회하여 영속성 컨텍스트에 영속화

2.조회의 주체가 되는 Entity만 SELECT 해서 영속화하기 때문에 연관 데이터가 필요하지 않은 상황에서 사용됨(DB에서는 JOIN이 발생하지만, 애플리케이션에서는 필터링된 Team 엔티티만 반환된다.)

멤버를 가진 team만 조회할때(inner join)

String query="select t From Team t join t.members";


결과: 팀1,팀A
팀2,팀B

페치 조인(fetch join)
1.조회의 주체가 되는 Entity 이외에 Fetch Join이 걸린 연관 Entity도 함께 SELECT 하여 모두 영속화

2.연관 엔티티를 참조하더라도 이미 영속성 컨텍스트에 있기 때문에 N+1 쿼리 문제 해소

5-4.페치 조인의 특징과 한계

  • 페치 조인 대상에는 별칭을 사용할수 없다. (Where 절로 필터링 불가)
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다. (페치 조인의 컬렉션은 1개만 지정 가능)
  • 컬렉션을 페치 조인하면 페이징 API를 사용할수 없다. (중복된 행들에 대한 잘못된 연산) -> 다대일로 뒤집어서 쿼리 후 페이징 or Batchsize

@BatchSize
엔티티의 연관된 컬렉션을 조회할 때, 여러 엔티티의 ID를 한 번에 모아서 IN 절을 사용해 해당 컬렉션의 모든 요소를 한 번의 쿼리로 가져온다. 이를 통해 페치 조인 없이 N+1 문제를 완화하고 JPA의 페이징 API와 함께 사용할 수 있다. -> 컬렉션 엔티티 조회 시점에 쿼리가 발생한다.

6.Named 쿼리

미리 정의해서 이름을 부여해두고 사용하는 JPQL이고 애플리케이션 로딩 시점에 쿼리를 파싱하기 때문에 문법 오류를 잡아주고 많은 이점이 있다.

public interface UserRepository extends JpaRepository<User,Long>{

	@Query("select u from User u where u.emailAddress = ?1")
	User findByEmailAddress(String emailAddress);

}

해당 findByEmailAddress를 실행하면 쿼리가 실행된다.

7.벌크 연산(executeUpdate())

벌크 연산이란 여러 개의 데이터를 한 번에 수정(Update), 삭제(Delete)하는 작업이다. 일반적으로 JPA에서는 엔티티를 하나씩 변경하면 1차 캐시(영속성 컨텍스트)를 거쳐야 하므로 성능이 저하될 수 있다. 벌크 연산을 사용하면 SQL을 직접 실행하여 여러 데이터를 한 번에 처리할 수 있다.

int result = em.createQuery("update Member m set m.age=20")
                 .executeUpdate();

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리 하기 때문에 벌크 연산을 먼저 수행하거나 수행 후 영속성 컨텍스트를 초기화 해줘야한다. -> 1차 캐시를 먼저 조회하기 때문에 잘못된 값이 나올수 있다.

Flush 자동 호출 조건

  • commit
  • query
  • flush()

0개의 댓글