[JPA] 자바 ORM 표준 JPA 프로그래밍 10장

xyzw·2023년 7월 31일
0

Spring

목록 보기
17/22

10.1 객체지향 쿼리 소개

JPA가 공식 지원하는 기능

  • JPQL(Java Persistence Query Language)
  • Criteria 쿼리: JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
  • 네이티브 SQL: JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다.

10.2 JPQL

가장 중요한 객체지향 쿼리 언어다.
JPQL을 사용하면 JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한다. 그리고 조회한 결과로 엔티티 객체를 생성해서 반환한다.

JPQL의 특징

  • 테이블이 아닌 엔티티 객체를 대상으로 검색하는 객체지향 쿼리
  • SQL과 비슷한 문법
  • ANSI 표준 SQL이 제공하는 기능을 유사하게 지원
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음
  • SQL보다 간결함: 엔티티 직접 조회, 묵시적 조인, 다형성 지원
@Entity(name="Member")
public class Member {
	
    @Column(name="name")
    private String username;
    ...
}

이 회원 엔티티를 대상으로 JPQL을 사용하는 예제를 보자.

String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList =
	em.createQuery(jpql, Member.class).getResultList();

회원이름이 kim인 엔티티를 조회한다. JPQL에서 Member는 엔티티 이름이다. 그리고 m.username은 테이블 컬럼명이 아니라 엔티티 객체의 필드명이다.

실제 실행된 SQL은 다음과 같다.

select
	member.id as id,
    member.age as age,
    member.team_id as team,
    member.name as name
from
	Member member
where
	member.name='kim'

JPQL 문법

select_문 :: =
	select_절
    from_절
    [where_절]
    [groupby_절]
    [having_절]
    [orderby_절]

update_문 ::= update_절 [where_절]
delete_문 ::= delete_절 [where_절]

SELECT 문

SELECT m FROM Member AS m where m.username = 'Hello'
  • 대소문자 구분
    엔티티와 속성은 대소문자를 구분한다.
    예를 들어 Member, username은 대소문자를 구분한다. 반면에 SELECT, FROM, AS 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
  • 엔티티 이름
    JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다.
  • 별칭은 필수
    Member AS m을 보면 Member에 m이라는 별칭을 주었다. JPQL은 별칭을 필수로 사용해야 한다.
    AS는 생략할 수 있다. 즉, Member m처럼 사용해도 된다.

TypeQuery, Query

작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다. 쿼리 객체는 TypeQuery와 Query가 있는데 반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 객체를 사용하고, 반환 타입을 명확하게 지정할 수 없으면 Query 객체를 사용하면 된다.

TypeQuery 사용

TypeQuery<Member> query = 
	em.createQuery("SELECT m FROM Member m", Member.class);

List<Member> resultList = query.getResultList();
for(Member member : resultList) {
	System.out.println("member = " + member);
}

Query 사용

Query query = 
	em.createQuery("SELECT m.username, m.age FROM Member m");
List<Member> resultList = query.getResultList();

for(Object o : resultList) {
	Object[] result = (Object[]) o;  //결과가 둘 이상이면 Object[] 반환
    System.out.println("username = " + result[0]);
	System.out.println("age = " + result[1]);
}

파라미터 바인딩

JPQL은 위치 기준 파라미터 바인딩과 이름 기준 파라미터 바인딩을 지원한다.

이름 기준 파라미터

파라미터를 이름으로 구분하는 방법이다. 앞에 :를 사용한다.

String usernameParam = "User1";

TypeQuery<Member> query = 
	em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class);

query.setParameter("username", usernameParam);
List<Member> resultList = query.getResultList();

위치 기준 파라미터

위치 기준 파라미터를 사용하려면 ? 다음에 위치 값을 주면 된다. 위치 값은 1부터 시작한다.
위치 기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.

List<Member> members = 
	em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class)
      .setParameter(1, usernameParam)
      .getResultList();

프로젝션

SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 하고 [SELECT {프로젝션 대상} FROM]으로 대상을 선택한다. 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다. 스칼라 타입은 숫자, 문자 등 기본 데이터 타입을 뜻한다.

페이징 API

페이징 처리용 SQL을 작성하는 일은 지루하고 반복적이다. 데이터베이스마다 페이징을 처리하는 SQL 문법이 다른 것도 문제이다.
JPA는 페이징을 다음 두 API로 추상화했다.

  • setFirstResult(int startPosition): 조회 시작 위치(0부터 시작한다)
  • setMaxResults(int maxResult): 조회할 데이터 수

집합과 정렬

집합은 집합함수와 함께 통계 정보를 구할 때 사용한다. 예를 들어 다음 코드는 순서대로 회원수, 나이 합, 평균 나이, 최대 나이, 최소 나이를 조회한다.

  • COUNT: 결과 수를 구한다. 반환 타입은 Long
  • MAX, MIN: 최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용한다.
  • AVG: 평균값을 구한다. 숫자타입만 사용할 수 있다. 반환타입은 Double
  • SUM: 합을 구한다. 숫자타입만 사용할 수 있다.
    반환타입 - 정수합 Long, 소수합 Double, BigInteger합 BigInteger, BigDeciaml합 BigDecimal

집합 함수 사용 시 참고사항

  • NULL 값은 무시하므로 통계에 잡히지 않는다.
  • 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL 값이 된다. 단, COUNT는 0이 된다.
  • DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다.
  • DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.

JPQL 조인

SQL 조인과 기능은 같고 문법만 약간 다르다.

내부 조인

INNER JOIN을 사용한다. INNER는 생략할 수 있다.

SELECT m
FROM Member m INNER JOIN m.team t
where t.name = :teamName

외부 조인

기능상 SQL의 외부 조인과 같다. OUTER는 생략 가능해서 보통 LEFT JOIN으로 사용한다.

SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t

컬렉션 조인

일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.

  • 회원->팀 으로의 조인은 다대일 조인이면서 단일 값 연관 필드를 사용한다.
  • 팀->회원 은 반대로 일대다 조인이면서 컬렉션 값 연관 필드를 사용한다.
SELECT t, m FROM Team t LEFT JOIN t.members m

여기서 t LEFT JOIN t.members는 팀과 팀이 보유한 회원목록을 컬렉션 값 연관 필드로 외부 조인했다.

세타 조인

WHERE 절을 사용해서 세타 조인을 할 수 있다. 세타 조인은 내부 조인만 지원한다.
세타 조인을 사용하면 전혀 관계 없는 엔티티도 조인할 수 있다.

SELECT count(m) from Member m, Team t
where m.username = t.name

JOIN ON 절

JPA 2.1부터 조인할 때 ON 절을 지원한다. ON 절을 사용하면 조인 대상을 필터링하고 조인할 수 있다. 보통 ON 절은 외부 조인에서만 사용한다.

모든 회원을 조회하면서 회원과 연관된 팀도 조회해보자.

select m, t from Member m
left join m.team t on t.name = 'A'

페치 조인

JPQL에서 성능 최적화를 위해 제공하는 기능이다. 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는데, join fetch 명령어로 사용할 수 있다.
그리고 페치 조인은 별칭을 사용할 수 없다.

페치 조인 문법

[LEFT [OUTER] | INNER] JOIN FETCH 조인경로

엔티티 페치 조인

페치 조인을 사용해서 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회해보자.

select m
from Member m join fetch m.team

컬렉션 페치 조인

일대다 관계인 컬렉션을 페치 조인해보자.

select t
from Team t join fetch t.members
where t.name = '팀A'

페치 조인과 DISTINCT

JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론이고 애플리케이션에서 한 번 더 중복을 제거한다.

select distinct t
from Team t join fetch t.members
where t.name = '팀A'

경로 표현식

경로 표현식: .을 찍어 객체 그래프를 탐색하는 것

경로 표현식의 용어 정리

  • 상태 필드: 단순히 값을 저장하기 위한 필드
  • 연관 필드: 연관관계를 위한 필드, 임베디드 타입 포함
    • 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티
    • 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션

경로 표현식과 특징

JPQL에서 경로 표현식을 사용해서 경로 탐색을 하려면 다음 3가지 경로에 따라 어떤 특징이 있는지 이해해야 한다.

  • 상태 필드 경로: 경로 탐색의 끝이다. 더는 탐색할 수 없다.
  • 단일 값 연관 경로: 묵시적으로 내부 조인이 일어난다. 단일 값 연관 경로는 계속 탐색할 수 있다.
  • 컬렉션 값 연관 경로: 묵시적으로 내부 조인이 일어난다. 더는 탐색할 수 없다. 단, FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있다.

경로 탐색을 사용한 묵시적 조인 시 주의사항

경로 탐색을 사용하면 묵시적 조인이 발생해서 SQL에서 내부 조인이 일어날 수 있다. 이때 주의사항은 다음과 같다.

  • 항상 내부 조인이다.
  • 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 한다.
  • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM 절에 영향을 준다.

조인이 성능상 차지하는 부분은 아주 크다. 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵다는 단점이 있다. 따라서 단순하고 성능에 이슈가 없으면 크게 문제가 안 되지만 성능이 중요하면 분석하기 쉽도록 묵시적 조인보다는 명시적 조인을 사용하자.

서브 쿼리

JPQL도 SQL처럼 서브 쿼리를 지원한다. 서브 쿼리를 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용할 수 없다.

서브 쿼리 함수

서브 쿼리는 다음 함수들과 같이 사용할 수 있다.

  • [NOT] EXISTS (subquery): 서브 쿼리에 결과가 존재하면 참이다.
  • {ALL | ANY | SOME} (subquery): 비교 연산자와 같이 사용한다.
  • [NOT] IN (subquery): 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참이다.

0개의 댓글