개요
JPA는 아래와 같이 다양한 쿼리 방법을 지원합니다.
- JPQL
- JPA Criteria
- QueryDSL
- Native SQL
- JDBC API 직접 적용
이번 게시글에서는 간단하게 위 쿼리 방법들을 설명한 후 JPQL 기본 문법과 기능에 대해 알아보겠습니다.
1. 다양한 쿼리 방법 소개
1.1 JPQL
- JPA가 테이블이 아닌 엔티티 객체를 중심으로 개발하는데, JPQL 역시 엔티티 객체를 대상으로 검색하는 쿼리 방법 (객체지향적인 것이 핵심)
- JPQL은 SQL을 추상화한 객체 지향 쿼리 언어
- 실제로 SQL 문법과 유사하여 ANSI 표준 키워드 전부 지원 (SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN)
- 앞서 언급한 대로 JPQL은 엔티티 객체를 대상으로 쿼리
- 반면, SQL은 DB 테이블을 대상으로 쿼리
- 모든 DB 데이터를 객체로 변환해서 검색하는 것은 실질적으로 불가능
- 따라서, 애플리케이션이 필요한 최소한의 데이터만 불러와야 함
1.2 Criteria
- JPQL로 Dynamic Query 작성하기 어렵기 때문에 등장한 개념
- 장점
- 문자가 아닌 자바 코드로 JPQL을 작성하므로 컴파일 시점에서 에러를 잡아줌
- 단점
- 아래 예시를 보면 알 수 있다시피 SQL스럽지 않고 복잡하며 실용성이 없음
- Criteria 대신 QueryDSL을 통해 동적 쿼리 작성하는 것을 추천
1.3 QueryDSL
- Criteria와 마찬가지로 문자가 아닌 자바코드로 JPQL을 작성하여 컴파일 단계에서 에러를 잡을 수 있음
- 동적 쿼리를 작성하기 쉬운 쿼리 언어
- 단순하고 난이도가 상대적으로 쉽기 때문에 실무 사용 권장
- 이후 별도로 공부하고 정리할 예정
1.4 Native SQL
- SQL을 직접 작성해서 사용하는 기능
- JPQL로 해결할 수 없는 특정 DB에 의존적인 기능
- ex) ORACLE의 CONNECT BY와 같이 특정 DB만 사용하는 SQL 힌트
- 컴파일 단계에서 에러를 잡을 수 없으므로 에러에 취약함
1.5 JDBC API 직접 적용
- JDBC 커넥션을 직접 사용하거나 스프링 JdbcTemplate, MyBatis 등을 함께 사용
- 단, 위와 같은 기능은 JPA와 연관이 없으므로 PersistenceContext를 강제로 플러쉬 해야 함
- 앞서 정리한 게시글 https://jaimemin.tistory.com/1898을 보면 데이터는 persist 메서드가 아닌 commit 메서드를 호출해야 DB에 들어감 (persist를 할 경우 영속성 컨텍스트에서 관리하지만 이는 JPA를 사용할 때만 접근 가능)
- 따라서, JPA와 연관 없는 쿼리를 호출할 때는 수동으로 flush를 진행해서 데이터를 실제 DB에 넣어줘야 오류 방지 가능
2. JPQL 기본 문법과 기능
2.1 JPQL 문법
- JPQL은 SQL을 추상화한 객체지향 언어이므로 기본적인 문법은 비슷
- ex) SELECT e FROM Employee AS e WHERE e.age > 30
- 엔티티와 속성은 위 예시처럼 대소문자 구분
- 엔티티: Employee
- 속성: age
- JPQL 키워드는 대소문자 구분을 하지 않지만 키워드는 대문자로 하는 것이 가독성 측면에서 유리
- 엔티티 중심으로 검색하므로 테이블명이 아닌 엔티티명을 사용
- @Entity(name = "XXX")라고 가정하면 From XXX
- 위 예시처럼 Alias는 필수 (Employee AS e)
- AS는 생략 가능
- 집합과 정렬을 위한 COUNT, SUM, AVG, MAX, MIN, GROUP BY, HAVING 그리고 ORDER BY 키워드 모두 지원
2.2 TypeQuery vs Query
- TypeQuery는 반환 타입이 명확할 때 사용
- Query의 경우 반환 타입이 불명확할 경우 사용
2.3 getResultList() vs getSingleResult()
- 두 메서드 모두 2,2에서 소개한 TypeQuery 혹은 Query의 메서드
- 결과가 하나 이상일 때는 리스트 형태로 반환하는 getResultList() 메서드 사용
- 결과가 없을 경우 빈 리스트 반환하므로 exception 발생 X
- 결과가 정확히 하나일 경우에는 단일 객체를 반환하는 getSingleResult() 메서드 사용
- 주의할 점은, 결과가 없을 경우나 결과가 둘 이상일 때 exception이 발생한다는 점
- 결과가 없을 경우 -> NoResultException 예외 발생
- 결과가 둘 이상일 경우 -> NonUniqueResultException 발생
- Hibernate의 경우 위와 같은 NoResultException 예외가 발생할 경우 catch 해서 결과가 없더라도 예외가 발생하지 않도록 처리
- 안전하게 처리하기 위해서는 getResultList() 메서드를 사용하는 것을 추천
2.4 파라미터 바인딩
- 이름 기준 혹은 위치 기준으로 파라미터를 바인딩해주는 기능
- 위치는 수시로 바뀔 수 있으므로 확실한 이름 기준으로 바인딩해주는 것을 추천
- 동적 쿼리를 작성할 때 사용하는 기능
2.5 프로젝션
- SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라고 함
- 프로젝션은 크게 3가지로 나눌 수 있음
- 엔티티 프로젝션
- 임베디드 타입 프로젝션
- 스칼라 타입 프로젝션
2.5.1 엔티티 프로젝션
- 이름에서도 유추할 수 있다시피 엔티티를 반환받는 SELECT 쿼리문
- 엔티티 프로젝션 시 조회된 모든 엔티티들은 PersistenceContext로 인해 관리가 됨
- PersistenceContext에 대한 자세한 설명은 https://jaimemin.tistory.com/1898 참고
2.5.2 임베디드 타입 프로젝션
- 앞서 https://jaimemin.tistory.com/1953에서 정리한 임베디드 값 타입을 조회하는 SELECT 쿼리문
- 아래 예시처럼 주소를 조회할 때 유용하게 쓰임
2.5.3 스칼라 타입 프로젝션
- 여러 타입의 값을 조회할 때 사용하는 SELECT 쿼리문
- 타입이 여러 개이므로 Object 배열을 반환받는 방법이 있고
- 혹은 스칼라 타입 프로젝션을 위한 별도 DTO를 생성하여 new 명령어를 통해 조회하는 방법이 있음
- Object 배열로 반환받는 방법보다 가독성 좋음
- 단, 패키지명을 포함한 전체 클래스명을 입력해야 하는 불편함이 존재하며 순서와 타입이 일치하는 생성자가 필요하다는 단점도 있음 => QueryDSL 사용 시 해결 가능
2.6 페이징 API
- JPA는 페이징을 아래의 두 API로 추상화
- setFirstResult(int startPosition): 조회 시작 위치
- setMaxResults(int maxResult): 조회할 데이터 수
- ORACLE의 페이징 처리에 비해 훨씬 간단함
- MySQL 혹은 PostgreSQL의 offset, limit과 유사
2.7 Join
- INNER JOIN, OUTER JOIN, CROSS JOIN 모두 지원
- JPA 2.1부터 아래의 ON 절을 활용한 JOIN 또한 지원
- 조인 대상 필터링
- 연관관계없는 엔티티 OUTER JOIN (Hibernate 5.1 ~)
2.8 SubQuery
- JPA 또한 SQL처럼 서브 쿼리를 지원하며 아래의 키워드를 제공
- [NOT] EXISTS (subquery)
- {ALL | ANY | SOME} (subquery)
- [NOT] IN (subquery)
- 하지만, SQL과 달리 JPQL은 서브 쿼리 한계가 존재
- JPA는 WHERE, HAVING 절에서만 서브 쿼리를 사용 가능
- Hibernate를 이용할 경우 SELECT 절에서도 서브 쿼리 사용 가능
- 하지만, FROM 절의 서브 쿼리는 현재 JPQL에서 사용 불가능
- Native Query를 사용하거나
- 쿼리를 두 번 호출하거나
- 혹은 쿼리를 호출한 뒤 Application 단에서 제어를 하는 방식으로 우회해야 함
2.9 JPQL Type
- 엔티티를 제외하고도 문자, 숫자, Boolean 타입 지원
- Enum 타입도 지원하지만 이를 위해서는 스칼라 타입 프로젝션처럼 전체 패키지명을 명시해야 하는 불편한 점이 있음
- 파라미터 바인딩을 통해 위 문제점을 해결 가능
- 엔티티 타입도 지원하는데 이는 @Discriminator 어노테이션을 통해 명시한 DTYPE을 의미
- 즉, 상속관계 매핑에서 사용되는 타입으로 자세한 내용은 https://jaimemin.tistory.com/1909 참고
2.10 사용자 정의 함수
- 각 DB 방언(Dialect)마다 사용하는 함수가 다름
- 따라서 특정 함수를 사용하기 위해서는 사용하는 DB 방언을 상속받은 뒤, 아래와 같이 사용자 정의 함수를 등록해야 함
- 아래의 예시는 H2Dialect에 group_concat을 추가한 예시
- CustomH2Dialect 클래스를 선언하고 H2Dialect를 상속받은 뒤 group_concat 함수를 등록
- persistence.xml에 CustomH2Dialect 등록
- 실제로 group_concat 함수를 호출
CustomH2Dialect
public class CustomH2Dialect extends H2Dialect {
public CustomH2Dialect() {
registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
}
}
persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="hello">
<properties>
<!-- 필수 속성 -->
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
<property name="hibernate.dialect" value="com.tistory.jaimemin.dialect.CustomH2Dialect"/>
<!-- 옵션 -->
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
group_concat 함수 호출하는 에시
출처
자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한 강사님)
반응형
'DB > JPA' 카테고리의 다른 글
[JPA] 준영속(Detached) 상태 엔티티 수정하는 방법 (0) | 2023.05.07 |
---|---|
[JPA] JPQL 추가 정리 (0) | 2021.10.18 |
[JPA] 값 타입 정리 (0) | 2021.09.29 |
[JPA] 프록시와 연관관계 관리 정리 (0) | 2021.09.14 |
[JPA] @MappedSuperclass (0) | 2021.09.07 |