꿈꾸는 새벽하늘

10장. 객체지향 쿼리 언어 본문

🌿 Spring & Spring Boot/📗 자바 ORM 표준 JPA 프로그래밍

10장. 객체지향 쿼리 언어

rovemin 2023. 7. 31. 23:42

1. 객체지향 쿼리 소개

ORM을 사용하면 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요하다. JPQL은 이러한 문제를 해결하기 위해 만들어졌다.

 

JPQL은 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리이고, SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다는 특징이 있다.

즉, SQL이 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리라면 JPQL은 엔티티 객체를 대상으로 하는 객체지향 쿼리이다.

 

JPA가 공식 지원하는 기능

  • JPQL: 객체지향 쿼리
  • Criteria 쿼리: JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
  • 네이티브 SQL: JPA에서 JPQL 대신 직접 SQL 사용 가능

JPA가 공식 지원하지는 않지만 알아두어야 하는 기능

  • QueryDSL: Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크
  • JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용: 필요 시 JDBC 직접 사용 가능

2. JPQL

JPQL은 엔티티 객체를 조회하는 객체지향 쿼리이다.

 

JPQL의 특징

  • JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다.
    • 데이터베이스 방언만 변경하면 JPQL을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있다.
  • JPQL은 SQL보다 간결하다
    • 엔티티 직접 조회, 묵시적 조인, 다형성 지원으로 SQL보다 코드가 간결하다.
  • JPQL은 결국 SQL로 변환된다.

1) JPQL 기본 문법

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

JPQL은 SQL과 비슷하게 SELECT, UPDATE, DELETE문을 사용할 수 있다. 엔티티를 저장할 때는 EntityManager.persist() 메소드를 사용하면 되므로 INSERT문은 없다.

JPQL에서 UPDATE, DELETE문은 벌크 연산이라고 한다.

SELECT문

SELECT m FROM Member AS m where m.username = 'Hello'
  • 대소문자 구분
    • 엔티티와 속성은 대소문자를 구분한다. (ex. Member, username)
    • 반면 SELECT, FROM, AS 등의 JPQL 키워드는 대소문자를 구분하지 않는다.
  • 엔티티 이름
    • Member는 클래스 명이 아닌 엔티티 명이다.
    • 엔티티 명은 @Entity(name = "XXX")로 지정할 수 있다.
    • 엔티티 명을 지정하지 않으면 클래스 명을 기본값으로 사용한다.
    • 기본값인 클래스 명을 엔티티 명으로 사용하는 것을 추천한다.
  • 별칭은 필수
    • Member에 m이라는 별칭을 부여한 것처럼 JPQL은 별칭을 필수로 사용해야 한다.

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 resultList = query.getResultList();

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

2) 파라미터 바인딩

위치 기준 파라미터 바인딩만 지원하는 JDBC와 다르게 JPQL은 이름 기준 파라미터 바인딩도 지원한다.

  • 이름 기준 파라미터
    • 이름 기준 파라미터는 앞에 :를 사용하여 파라미터를 이름으로 구분한다.
  • 위치 기준 파라미터
    • ? 다음에 위치 값을 주어서 파라미터를 위치로 바인딩한다.

이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.

3) 프로젝션

SELECT절에 조회할 대상을 지정하는 것을 프로젝션(projection)이라고 한다.

프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 있고, [SELECT {프로젝션 대상} FROM]으로 대상을 선택한다.

4) 페이징 API

데이터베이스마다 페이징을 처리하는 SQL 문법이 달라서 페이징 처리용 SQL을 작성하는 것은 쉽지 않다.

JPA는 페이징을 두 API로 추상화했다.

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

5) 집합과 정렬

집합은 집합 함수와 함께 통계 정보를 구할 때 사용한다.

 

집합 함수

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

집합 함수 사용 시 참고사항

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

GROUP BY, HAVING

GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다. HAVING은 GROUP BY와 함게 사용하는데 GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링한다.

/* 그룹별 통계 데이터 중 평균 나이가 10살 이상인 그룹 조회 */
select t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
from Member m LEFT JOIN m.team t
GROUP BY t.name
HAVING AVG(m.age) >= 10

정렬(ORDER BY)

ORDER BY는 결과를 정렬할 때 사용한다.

orderby_절 :: = ORDER BY { 상태필드 경로 | 결과 변수 [ ASC | DESC ] } +
  • ASC: 오름차순 (기본값)
  • DESC: 내림차순

6) JPQL 조인

내부 조인

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

String teamName = "팀A";
String query = "SELECT m FROM Member m INNER JOIN m.team t" + "WHERE t.name = :teamName";

List<Member> members = em.createQuery(query, Member.class)
    .setParameter("teamName", teamName)
    .getResultList();

외부 조인

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

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

컬렉션 조인

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

  • [회원 -> 팀]으로의 조인은 다대일 조인이면서 단일 값 연관 필드(m.team)를 사용한다.
  • [팀 -> 회원]은 일대다 조인이면서 컬렉션 값 연관 필드(m.members)를 사용한다.
/* 팀과 팀이 보유한 회원목록을 컬렉션 값 연관 필드로 외부 조인 */
SELECT t, m FROM Team t LEFT JOIN t.members m

세타 조인

WHERE 절을 사용한다. 세타 조인은 내부 조인만 지원한다.

세타 조인을 사용하면 전혀 관계 없는 엔티티도 조인할 수 있다.

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

JOIN ON 절

JPA 2.1부터 조인할 때 ON 절을 지원한다. 조인 시 ON 절을 사용하면 조인 대상을 필터링하고 조인할 수 있다.

참고로 내부 조인의 ON 절은 WHERE 절을 사용할 때와 결과가 같으므로 보통 ON 절은 외부 조인에서만 사용한다.

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

7) 페치 조인

페지 조인은 JPQL에서 성능 최적화를 위해 제공하는 기능이다.

연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능으로, join fetch 명령어로 사용할 수 있다.

페치 조인 :: = [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로

3. Criteria

Criteria는 JPQL을 생성하는 빌더 클래스다.

문자가 아닌 query.select(m).where(...)과 같은 프로그래밍 코드로 JPQL을 작성할 수 있다.

 

Criteria의 특징

  • 문자가 아닌 코드로 JPQL을 작성하므로 컴파일 시점에 오류를 발견할 수 있다.
  • IDE를 사용하면 코드 자동완성을 지원한다.
  • 동적 쿼리를 작성하기 편하다.

4. QueryDSL

QueryDSL은 Criteria처럼 JPQL 빌더 역할을 하는데 코드 기반이면서 단순하고 사용하기 쉽다.

오픈소스 프로젝트인 QueryDSL은 HQL(하이버네이트 쿼리언어)을 시작으로 지금은 JPA, JDO, JDBC, Lucene, Hibernate Search, 몽고DB, 자바 컬렉션 등을 지원한다.

QueryDSL은 데이터를 조회하는 기능이 특화되어 있다.

5. 네이티브 SQL

네이티브 SQL은 JPA가 SQL을 직접 사용할 수 있도록 지원하는 기능이다.

네이티브 SQL을 사용하면 엔티티를 조회할 수 있고 JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있다.

6. 벌크 연산

벌크 연산은 여러 데이터를 한 번에 수정하거나 삭제한다.

벌크 연산은 executeUpdate() 메소드를 사용한다. 이 메소드는 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다.

벌크 연산을 사용할 때는 벌크 연산이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점을 주의해야 한다.