2025.03.27
JPA의 가장 큰 한계 중 하나는 SQL의 SELECT 구문처럼 복잡한 조회 쿼리를 자유롭게 작성하기 어렵다.
JPA는 객체 중심 매핑을 제공하지만, 기본적으로는 단순한 CRUD 중심의 API를 제공하기 때문에 복잡한 조건 검색이나 조인을 표현하기엔 한계가 있다.
-> JPQL(Java Persistence Query Language) 해결
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.2">
<!-- EntityManagerFactory를 식별하기 위한 이름 지정
(JPA 설정 시 참조되는 식별자) -->
<persistence-unit name="퍼시스턴스유닛이름">
<!-- JPA가 관리할 엔티티 클래스들 등록 -->
<class>엔티티패키지경로.엔티티클래스1</class>
<class>엔티티패키지경로.엔티티클래스2</class>
<properties>
<!-- 데이터베이스 연결 설정 정보 -->
<property name="jakarta.persistence.jdbc.driver" value="JDBC드라이버클래스명"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/DB이름"/>
<property name="jakarta.persistence.jdbc.user" value="DB사용자ID"/>
<property name="jakarta.persistence.jdbc.password" value="DB비밀번호"/>
<!-- SQL 로그 출력 설정 -->
<!-- 실행 SQL 출력 -->
<property name="hibernate.show_sql" value="true"/>
<!-- SQL 보기 좋게 정렬 출력 -->
<property name="hibernate.format_sql" value="true"/>
<!-- DDL 생성 전략 설정 -->
<property name="hibernate.hbm2ddl.auto" value="none"/>
<!--
개발 환경에서 주로 사용하는 값
- create : 실행 시 기존 테이블 DROP 후 재생성
- create-drop : 실행 시 생성, 종료 시 DROP
- update : 변경된 부분만 반영 (보존된 데이터와 함께)
운영 환경에서 주로 사용하는 값
- validate : 테이블/컬럼 유효성 검사만 (DDL 생성 X)
- none : 아무 작업도 하지 않음 (기본값),
DB 테이블과 일치하게 작성
-->
</properties>
</persistence-unit>
</persistence>
@Entity(name = "엔티티논리이름")
<!-- 실제 DB 테이블 이름 -->
@Table(name = "테이블명")
public class 엔티티클래스명 {
@Id
@Column(name = "기본키컬럼명")
private 기본키자료형 기본키필드명;
@Column(name = "일반컬럼명1")
private 자료형 필드명1;
@Column(name = "일반컬럼명2")
private 자료형 필드명2;
@Column(name = "일반컬럼명3")
private 자료형 필드명3;
@Column(name = "일반컬럼명4")
private 자료형 필드명4;
<!-- 기본 생성자
public 엔티티클래스명() {}
<!-- 모든 필드를 초기화하는 생성자 -->
public 엔티티클래스명(기본키자료형 기본키필드명, 자료형 필드명1, 자료형 필드명2,
자료형 필드명3, 자료형 필드명4) {
super(): // Object의 기본 생성자 호출, 없어도 무방
this.기본키필드명 = 기본키필드명;
this.필드명1 = 필드명1;
this.필드명2 = 필드명2;
this.필드명3 = 필드명3;
this.필드명4 = 필드명4;
}
<!-- Getter / Setter 생략 가능 (Lombok 사용 시 @Getter, @Setter 어노테이션 활용) -->
@Override
public String toString() {
return "엔티티클래스명 [기본키필드명=" + 기본키필드명 +
", 필드명1=" + 필드명1 +
", 필드명2=" + 필드명2 +
", 필드명3=" + 필드명3 +
", 필드명4=" + 필드명4 + "]";
}
}
public class SimpleJPQLTests {
private static EntityManagerFactory entityManagerFactory;
private static EntityManager entityManager;
@BeforeAll
public static void initFactory() {
<!-- persistence.xml의 persistence-unit 이름 사용 -->
entityManagerFactory = Persistence.createEntityManagerFactory("퍼시스턴스유닛이름");
}
@BeforeEach
public void initManager() {
entityManager = entityManagerFactory.createEntityManager();
}
@AfterAll
public static void closeFactory() {
entityManagerFactory.close();
}
@AfterEach
public void closeManager() {
entityManager.close();
}
<!-- 여러 테스트 -->
}
@Test
public void TypedQuery를_이용한_단일메뉴_조회_테스트() {
<!-- given -->
<!-- FROM 엔티티명 으로 작성, FROM 테이블 X -->
String jpql변수 = "SELECT 별칭.필드명 FROM 엔티티논리이름 별칭 WHERE 별칭.기본키필드 = 기본키값";
<!-- when : TypedQuery 생성 및 단일 결과 조회 -->
TypedQuery<필드자료형> query변수 = entityManager.createQuery(jpql변수, 필드자료형.class);
필드자료형 조회결과변수 = query.getSingleResult();
<!-- then : 조회 결과 검증 -->
assertEquals("기대값", 조회결과변수);
}
@Test
public void Query를_이용한_단일메뉴_조회_테스트() {
<!-- given : JPQL 작성 -->
String jpql변수 = "SELECT 별칭.조회필드 FROM 엔티티논리이름 별칭 WHERE 별칭.기본키필드 = 기본키값";
<!-- when : 타입 미지정 일반 Query 객체로 조회 -->
<!-- 반환 타입을 명시하지 않음 -->
Query query변수 = entityManager.createQuery(jpql변수);
<!-- Object 타입으로 반환됨 -->
Object 조회결과변수 = query변수.getSingleResult();
<!-- then : 타입 확인 및 검증 -->
assertTrue(조회결과변수 instanceof 반환타입);
assertEquals("예상값", 조회결과변수);
}
@Test
public void TypedQuery_이용_단일행_조회_테스트() {
<!-- given : JPQL 작성 -->
String jpql변수 = "SELECT 별칭 FROM 엔티티논리이름 별칭 WHERE 별칭.기본키필드 = 기본키값";
<!-- when : 반환 타입을 엔티티 클래스 타입으로 지정 -->
TypedQuery<엔티티클래스> query변수 = entityManager.createQuery(jpql변수, 엔티티클래스.class);
<!-- 단일 엔티티 객체 조회 -->
엔티티클래스 조회결과변수 = query변수.getSingleResult();
<!-- then : 필드값 검증 및 출력 -->
assertEquals(기대값, 조회결과변수.get검증필드());
System.out.println(조회결과변수);
}
@Test
public void TypedQuery_이용_다중행_조회_테스트() {
<!-- given : JPQL 작성 -->
String jpql변수 = "SELECT 별칭 FROM 엔티티논리이름 별칭";
<!-- when : 반환 타입을 엔티티 클래스 타입으로 지정하여 다중 결과 조회 -->
TypedQuery<엔티티클래스> query변수 = entityManager.createQuery(jpql변수, 엔티티클래스.class);
<!-- 다중 결과 조회 → List로 반환 -->
List<엔티티클래스> 조회결과목록변수 = query변수.getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(조회결과목록변수);
조회결과목록변수.forEach(System.out::println);
}
@Test
public void Query_이용_다중행_조회_테스트() {
<!-- given : 조회할 JPQL 작성 -->
String jpql변수 = "SELECT 별칭 FROM 엔티티논리이름 별칭";
<!-- when : Query 객체 생성
(반환 타입 명시 X → Object 목록 반환) -->
Query query변수 = entityManager.createQuery(jpql변수);
<!-- 다중 결과 조회 → List로 반환 (타입 캐스팅 주의) -->
List<엔티티클래스> 조회결과목록변수 = query변수.getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(조회결과목록변수);
조회결과목록변수.forEach(System.out::println);
}
@Test
public void distinct_활용_중복제거_다중행_조회_테스트() {
<!-- given : 중복 제거 대상 필드를 선택한 JPQL 작성 -->
String jpql변수 = "SELECT DISTINCT 별칭.필드명 FROM 엔티티논리이름 별칭";
<!-- when : TypedQuery 생성 (반환할 단일 필드의 타입 지정) -->
TypedQuery<필드자료형> query변수 = entityManager.createQuery(jpql변수, 필드자료형.class);
<!-- 결과 조회 (중복 제거된 값 리스트) -->
List<필드자료형> 결과목록변수 = query변수.getResultList();
<!-- then : 검증 및 출력 -->
assertNotNull(결과목록변수);
결과목록변수.forEach(System.out::println);
}
@Test
public void in연산자_활용_조건_조회_테스트() {
<!-- given : 특정 필드에 대해 여러 값 조건으로 조회 -->
String jpql변수 = "SELECT 별칭 FROM 엔티티논리이름 별칭 WHERE 별칭.필드명 IN (값1, 값2, ...)";
<!-- when : 결과 조회 -->
List<엔티티클래스> 결과목록변수 = entityManager
.createQuery(jpql변수, 엔티티클래스.class)
.getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(결과목록변수);
결과목록변수.forEach(System.out::println);
}
@Test
public void like연산자_문자열패턴_조회_테스트() {
<!-- given : 특정 문자열을 포함하는 레코드 조회 -->
String jpql변수 = "SELECT 별칭 FROM 엔티티논리이름 별칭 WHERE 별칭.문자열필드명 LIKE '%포함할문자열%'";
<!-- when : 쿼리 실행 및 결과 반환 -->
List<엔티티클래스> 결과목록변수 = entityManager
.createQuery(jpql변수, 엔티티클래스.class)
.getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(결과목록변수);
결과목록.forEach(System.out::println);
}
JPQL에서 조건절에 고정값 대신 변수(파라미터) 를 사용하여 동적으로 값을 주입
-> 사용자 입력 값이나 조건에 따라 유연한 쿼리 구성 가능
@Test
public void 이름_기준_파라미터_바인딩_메뉴_목록_조회_테스트() {
<!-- given : 파라미터로 전달할 값 설정 -->
String 파라미터변수 = "검색할이름값";
<!-- when : 이름 기준 파라미터를 사용한 JPQL 작성 및 실행 -->
String jpql변수 = "SELECT 별칭 FROM 엔티티논리이름 별칭 WHERE 별칭.필드명 = :파라미터이름";
List<엔티티클래스> 결과리스트변수 = entityManager
.createQuery(jpql변수, 엔티티클래스.class)
.setParameter("파라미터이름", 파라미터변수)
.getResultList();
<!-- then : 결과 검증 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(System.out::println);
}
@Test
public void 위치_기준_파라미터_바인딩_메뉴_목록_조회_테스트() {
<!-- given : 조회 조건으로 사용할 파라미터 값 설정 -->
자료형 파라미터변수 = "검색할값";
<!-- when : 위치 기반 파라미터(?1 등)를 사용하는 JPQL 작성 및 실행 -->
String jpql변수 = "SELECT 별칭 FROM 엔티티논리이름 별칭 WHERE 별칭.필드명 = ?1";
List<엔티티클래스> 결과리스트변수 = entityManager
.createQuery(jpql변수, 엔티티클래스.class)
.setParameter(1, 파라미터변수)
.getResultList();
<!-- then : 조회된 결과 검증 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(System.out::println);
}
SELECT 절에 조회할 대상을 지정하는 것
@Test
public void 단일_엔티티_프로젝션_조회_및_수정_테스트() {
<!-- when : 엔티티 전체를 조회 -->
<!-- SELECT 대상이 엔티티 전체 -->
String jpql변수 = "SELECT 별칭 FROM 엔티티논리이름 별칭";
List<엔티티클래스명> 결과리스트변수 = entityManager.createQuery(jpql변수, 엔티티클래스명.class)
.getResultList();
<!-- then : 조회 결과 검증 및 수정 테스트 -->
assertNotNull(결과리스트변수);
<!-- 엔티티 프로젝션은 영속성 컨텍스트에서 관리되므로,
값을 수정하면 DB에도 반영됨 -->
EntityTransaction 트랜잭션변수 = entityManager.getTransaction();
트랜잭션변수.begin();
<!-- 2번째 엔티티 객체의 필드값 수정 -->
결과리스트변수.get(1).set필드명("수정값");
트랜잭션변수.commit();
}
@Test
public void 양방향_연관관계_엔티티_프로젝션_테스트() {
<!-- given : 조건 파라미터 설정 -->
기본키자료형 파라미터이름 = 특정기본키값;
<!-- when : 양방향 연관관계에서 연관된 엔티티를 조회 -->
String jpql변수 = "SELECT 별칭.연관필드 FROM 주인엔티티논리이름 별칭 WHERE 별칭.기본키필드 = :파라미터이름";
비주인엔티티클래스 연관객체변수 = entityManager.createQuery(jpql변수, 비주인엔티티클래스.class)
.setParameter("파라미터이름", 파라미터이름)
.getSingleResult();
<!-- then : 역방향 탐색 및 검증 -->
assertNotNull(연관객체변수);
System.out.println(연관객체변수);
<!-- 연관된 주인 엔티티 리스트 역방향으로 조회 -->
assertNotNull(연관객체변수.get주인엔티티리스트());
연관객체.get주인엔티티리스트().forEach(System.out::println);
}
@Test
public void 임베디드_타입_프로젝션_테스트() {
<!-- when : 임베디드 타입 필드만 조회하는 JPQL 작성 -->
String jpql변수 = "SELECT 별칭.임베디드필드 FROM 엔티티논리이름 별칭";
List<임베디드클래스> 임베디드객체리스트변수 =
entityManager.createQuery(jpql변수, 임베디드클래스.class).getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(임베디드객체리스트변수);
임베디드객체리스트변수.forEach(System.out::println);
}
@Test
public void 스칼라_타입_프로젝션_테스트() {
<!-- when : 특정 컬럼(기본 타입)만 조회하는 JPQL 작성 -->
String jpql변수 = "SELECT 별칭.컬럼명 FROM 엔티티논리이름 별칭";
List<컬럼자료형> 결과리스트변수 = entityManager
.createQuery(jpql변수, 컬럼자료형.class)
.getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(결과리스트변수);
결과리스트.forEach(System.out::println);
}
@Test
public void 스칼라_타입_다중컬럼_조회_테스트() {
<!-- when : 여러 기본형 컬럼 조회 시 Query 사용 (TypedQuery 불가) -->
String jpql변수 = "SELECT 별칭.컬럼1, 별칭.컬럼2 FROM 엔티티논리이름 별칭";
List<Object[]> 결과리스트변수 = entityManager.createQuery(jpql변수).getResultList();
<!-- then : 각 행(Object[])의 컬럼값 출력 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(행 -> {
Arrays.stream(행).forEach(System.out::println);
});
}
@Test
public void new명령어_활용_DTO프로젝션_테스트() {
<!-- when : 조회할 컬럼들을 DTO의 생성자 파라미터로 넘김 -->
String jpql변수 = "SELECT new 패키지명.DTO클래스명(별칭.컬럼1, 별칭.컬럼2) FROM 엔티티논리이름 별칭";
List<DTO클래스명> 결과리스트변수 = entityManager
.createQuery(jpql변수, DTO클래스명.class)
.getResultList();
<!-- then : 결과 확인 -->
assertNotNull(결과리스트변수);
결과리스트.forEach(System.out::println);
}

JPA에서는 대량의 데이터를 조회할 때 페이지 단위로 나누어 조회하는 기능을 제공
@Test
public void 페이징_API_활용_조회_테스트() {
<!-- given : 조회 시작 위치(offset)와 최대 조회 수(limit) 설정 -->
int 시작위치변수 = 10; // 몇 번째부터 조회할지
int 조회수변수 = 5; // 몇 개를 가져올지
<!-- when : JPQL로 정렬 포함 조회 후 페이징 적용 -->
String jpql변수 = "SELECT 별칭 FROM 엔티티논리이름 별칭 ORDER BY 별칭.정렬기준필드 DESC";
List<엔티티클래스명> 결과리스트변수 = entityManager
.createQuery(jpql변수, 엔티티클래스명.class)
<!-- 조회 시작 위치 설정 -->
.setFirstResult(시작위치변수) // offset (0부터 시작)
<!-- 가져올 최대 개수 설정 -->
.setMaxResults(조회수변수) // limit
.getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(System.out::println);
}
COUNT, MAX, MIN, SUM, AVG로 SQL의 그룹함수와 별반 차이가 없다
1. 반환 자료형
- COUNT 등 정수 연산 → Long
- SUM, AVG 등 실수 연산 → Double
- 반환 값을 받을 변수도 Long, Double과 같은 래퍼 클래스로 선언해야 한다.
@Test
@DisplayName("특정 조건의 엔티티 수를 COUNT로 조회하는 테스트")
public void 조건에_따른_엔티티_개수_조회_테스트() {
<!-- given : 조건값(예: 외래키 등) 설정 -->
조건자료형 조건파라미터변수 = 특정값;
<!-- when : COUNT를 사용하여 해당 조건의 데이터 개수 조회 -->
String jpql변수 = "SELECT COUNT(별칭.필드명) FROM 엔티티논리이름 별칭 WHERE 별칭.조건필드 = :조건파라미터";
Long 개수조회결과변수 = entityManager.createQuery(jpql변수, Long.class)
.setParameter("조건파라미터", 조건파라미터)
.getSingleResult();
<!-- then : 결과 검증 및 출력 -->
assertTrue(개수조회결과변수 >= 0); // 결과는 0 이상이어야 함
System.out.println("조회된 개수 = " + 개수조회결과변수);
}
@Test
@DisplayName("그룹함수(COUNT 제외)의 조회 결과가 없을 경우 NPE 테스트")
public void 그룹함수_결과없음_NPE_테스트() {
<!-- given : 조건에 해당하는 데이터가 없는 값 설정 -->
조건자료형 조건파라미터변수 = 특정값;
<!-- when : 조건을 만족하는 데이터가 없을 경우, SUM, AVG, MIN, MAX는 null 반환 -->
String jpql변수 = "SELECT 그룹함수(별칭.필드명) FROM 엔티티논리이름 별칭 WHERE 별칭.조건필드 = :조건파라미터";
<!-- then : 기본 자료형 사용 시 언박싱 과정에서 NullPointerException 발생 -->
assertThrows(NullPointerException.class, () -> {
기본자료형 결과변수 = entityManager.createQuery(jpql변수, 그룹함수반환형.class)
.setParameter("조건파라미터", 조건파라미터)
.getSingleResult(); // null → 언박싱 NPE 발생
});
<!-- then : 참조형(Wrapper 클래스) 사용 시 안전하게 null 처리 가능 -->
assertDoesNotThrow(() -> {
그룹함수반환형 결과변수 = entityManager.createQuery(jpql변수, 그룹함수반환형.class)
.setParameter("조건파라미터", 조건파라미터)
.getSingleResult(); // null 반환됨
});
}
@Test
@DisplayName("GROUP BY 및 HAVING 절을 활용한 그룹 함수 조건 조회 테스트")
public void 그룹함수_HAVING_조건조회_테스트() {
<!-- given : 그룹 조건 비교를 위한 최소값 설정 (그룹 함수 반환형에 맞게 Wrapper 타입 사용) -->
그룹함수반환형 조건파라미터변수 = 조건값;
<!-- when : 특정 그룹 조건을 만족하는 레코드를 GROUP BY, HAVING 절로 조회 -->
String jpql변수 = "SELECT 별칭.그룹기준필드, 그룹함수(별칭.계산필드)" +
" FROM 엔티티논리이름 별칭" +
" GROUP BY 별칭.그룹기준필드" +
" HAVING 그룹함수(별칭.계산필드) >= :조건파라미터";
List<Object[]> 결과리스트변수 = entityManager.createQuery(jpql변수, Object[].class)
.setParameter("조건파라미터", 조건파라미터)
.getResultList();
<!-- then : 결과가 null이 아니고, 각 그룹에 대한 결과값 출력 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(행 -> {
Arrays.stream(행).forEach(System.out::println);
});
}
일반 조인
@Test
@DisplayName("내부조인을 이용한 조회 테스트")
public void innerJoinSelectTest() {
<!-- when : 내부 조인을 활용한 JPQL 작성 -->
String jpql변수 = "SELECT 별칭1 FROM 엔티티논리이름 별칭1 JOIN 별칭1.연관필드명 별칭2";
List<엔티티클래스명> 결과리스트변수 = entityManager
.createQuery(jpql변수, 엔티티클래스명.class)
.getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(System.out::println);
}
@Test
@DisplayName("외부 조인을 이용한 조회 테스트")
public void outerJoinSelectTest() {
<!--
외부 조인(LEFT JOIN 또는 RIGHT JOIN)을 사용하면
- 연관된 엔티티가 없어도 기준 엔티티의 정보는 모두 조회된다.
- SELECT 절에서 엔티티가 아닌 개별 필드(스칼라 값)를 선택한 경우,
결과는 Object[]로 반환된다.
-->
<!-- when : 외부 조인을 포함한 JPQL 작성 -->
String jpql변수 = "SELECT 주인엔티티별칭.조회필드1, 연관엔티티별칭.조회필드2 "
+ "FROM 주인엔티티논리이름 주인엔티티별칭 "
+ "RIGHT JOIN 주인엔티티별칭.연관필드 연관엔티티별칭 "
+ "ORDER BY 주인엔티티별칭.연관필드.정렬기준필드";
List<Object[]> 결과리스트변수 = entityManager.createQuery(jpql변수, Object[].class).getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(행 -> {
Arrays.stream(행).forEach(컬럼 -> System.out.print(컬럼 + " "));
System.out.println();
});
}
@Test
@DisplayName("컬렉션 조인을 이용한 조회 테스트")
public void collectionJoinSelectTest() {
<!--
컬렉션 조인(OneToMany 조인)
- 컬렉션을 가지는 비주인 엔티티를 기준으로 조인하는 방식
- LEFT JOIN을 통해 컬렉션에 속한 값이 없어도 기준 엔티티 정보는 조회된다.
- SELECT 절에서 스칼라 타입을 지정한 경우 Object[]로 받아야 함
-->
<!-- when : 컬렉션 조인을 포함한 JPQL 작성 -->
String jpql변수 = "SELECT 비주인엔티티별칭.조회필드1, 주인엔티티별칭.조회필드2 "
+ "FROM 비주인엔티티논리이름 비주인엔티티별칭 "
+ "LEFT JOIN 비주인엔티티별칭.컬렉션필드명 주인엔티티별칭";
List<Object[]> 결과리스트변수 = entityManager.createQuery(jpql변수, Object[].class).getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(행 -> {
Arrays.stream(행).forEach(컬럼 -> System.out.print(컬럼 + " "));
System.out.println();
});
}
@Test
@DisplayName("세타 조인을 이용한 조회 테스트")
public void thetaJoinSelectTest() {
<!--
세타 조인 (Theta Join)
- 연관관계가 없는 엔티티들을 조인할 때 사용
- 크로스 조인과 동일
-->
<!-- when : 세타 조인을 이용해 모든 경우의 수 조회 -->
String jpql변수 = "SELECT 엔티티1별칭.출력필드1, 엔티티2별칭.출력필드2 "
+ "FROM 엔티티1논리이름 엔티티1별칭, 엔티티2논리이름 엔티티2별칭";
List<Object[]> 결과리스트변수 = entityManager.createQuery(jpql변수, Object[].class).getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(행 -> {
Arrays.stream(행).forEach(컬럼 -> System.out.print(컬럼 + " "));
System.out.println();
});
}
@Test
@DisplayName("페치 조인을 이용한 연관 엔티티 조회 테스트")
public void fetchJoinSelectTest() {
<!-- when : 연관된 엔티티까지 즉시 로딩하는 JPQL 작성 -->
String jpql변수 = "SELECT 주인엔티티별칭 FROM 주인엔티티논리이름 주인엔티티별칭 "
+ "JOIN FETCH 주인엔티티별칭.연관필드 비주인엔티티별칭";
List<주인엔티티클래스> 결과리스트변수 = entityManager
.createQuery(jpql변수, 주인엔티티클래스.class)
.getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(System.out::println);
}
select, from절에서는 사용할 수 없고 where, having절에서만 사용이 가능
JPQL 서브쿼리 한계
1. 가독성 저하
- 복잡한 쿼리일수록 가독성이 급격히 떨어짐
- 서브쿼리/동적 쿼리 구성 시 문자열 처리가 복잡해짐
2. 문자열 기반으로 -> 컴파일 시 문법 오류 확인 불가
- 쿼리는 문자열로 작성되므로 컴파일 단계에서는 문법 검증이 되지 않는다.
- 런타임 시점에만 오류 발생한다. -> SQLSyntaxErrorException
3. 띄어쓰기 / 구문 오류 민감
- 작은 오타, 공백 오류 등으로 쉽게 에러 발생
QueryDSL 한계 해결
@Test
@DisplayName("서브쿼리를 활용한 조건 기반 엔티티 조회 테스트")
public void 서브쿼리_조건조회_테스트() {
<!-- given : 서브쿼리에 사용할 비교 대상 파라미터 설정 -->
String 비교대상파라미터 = "기준값";
<!-- when : 서브쿼리를 사용하여 메인 쿼리 조건 필터링 -->
String jpql변수 = "SELECT 별칭1 FROM 엔티티1 별칭1 WHERE 별칭1.필드명 = "
+ "(SELECT 별칭2.비교필드 FROM 엔티티2 별칭2 WHERE 별칭2.조건필드 = :파라미터이름)";
List<엔티티1> 결과리스트변수 = entityManager.createQuery(jpql변수, 엔티티1.class)
.setParameter("파라미터이름", 비교대상파라미터)
.getResultList();
<!-- then : 조회 결과 검증 -->
assertNotNull(결과리스트변수);
결과리스트.forEach(System.out::println);
}
동적 쿼리 (Dynamic Query)
정적 쿼리 (Static Query) - NamedQuery
@Entity(name = "엔티티논리이름")
@Table(name = "테이블명")
@NamedQueries({
@NamedQuery(name = "엔티티논리이름.조회쿼리이름", query = "SELECT 별칭 FROM 엔티티논리이름 별칭")
})
public class 엔티티클래스명 {
@Id
@Column(name = "기본키컬럼명")
private 기본키자료형 기본키필드;
@Column(name = "일반컬럼명1")
private 자료형 필드명1;
@Column(name = "일반컬럼명2")
private 자료형 필드명2;
<!-- 기본 생성자 -->
public 엔티티클래스명() {}
<!-- 모든 필드를 매개변수로 받는 생성자 -->
public 엔티티클래스명(기본키자료형 기본키필드, 자료형 필드명1, 자료형 필드명2) {
super();
this.기본키필드 = 기본키필드;
this.필드명1 = 필드명1;
this.필드명2 = 필드명2;
}
<!-- Getter / Setter / toString() 생략 -->
테스트 코드 작성
public class NamedQueryTests {
private static EntityManagerFactory entityManagerFactory;
private static EntityManager entityManager;
@BeforeAll
public static void initFactory() {
<!-- persistence.xml의 persistence-unit 이름 사용 -->
entityManagerFactory = Persistence.createEntityManagerFactory("퍼시스턴스유닛이름");
}
@BeforeEach
public void initManager() {
entityManager = entityManagerFactory.createEntityManager();
}
@AfterAll
public static void closeFactory() {
entityManagerFactory.close();
}
@AfterEach
public void closeManager() {
entityManager.close();
}
<!-- 여러 테스트 -->
}
@Test
@DisplayName("문자열 조합 방식의 동적 JPQL 쿼리 테스트")
public void 동적조건_문자열JPQL_조회_테스트() {
<!-- given : 검색 조건 설정 -->
String 검색조건1 = "검색값1";
int 검색조건2 = 0;
<!-- when : 문자열 기반 동적 JPQL 생성 -->
StringBuilder jpql변수 = new StringBuilder("SELECT 별칭 FROM 엔티티논리이름 별칭 ");
<!-- 조건에 따라 WHERE 절 조합 -->
if (검색조건1 != null && !검색조건1.isEmpty() && 검색조건2 > 0) {
jpql변수.append("WHERE ");
jpql변수.append("별칭.필드1 LIKE '%' || :조건1 || '%' ");
jpql변수.append("AND ");
jpql변수.append("별칭.필드2= :조건2 ");
} else {
if (검색조건1 != null && !검색조건1.isEmpty()) {
jpql변수.append("WHERE ");
jpql변수.append("별칭.필드1 LIKE '%' || :조건1 || '%' ");
} else if (검색조건2 > 0) {
jpql변수.append("WHERE ");
jpql변수.append("별칭.필드2 = :조건2 ");
}
}
<!-- JPQL 실행 객체 생성 -->
TypedQuery<엔티티클래스명> 쿼리객체변수 = entityManager.createQuery(jpql변수.toString(), 엔티티클래스명.class);
<!-- 파라미터 바인딩 -->
if (검색조건1 != null && !검색조건1.isEmpty()) {
쿼리객체변수.setParameter("조건1", 검색조건1);
}
if (검색조건2 > 0) {
쿼리객체변수.setParameter("조건2", 검색조건2);
}
List<엔티티클래스명> 결과리스트변수 = 쿼리객체변수.getResultList();
<!-- then : 결과 출력 및 검증 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(System.out::println);
}
@Test
@DisplayName("네임드 쿼리를 이용한 정적 JPQL 조회 테스트")
public void 네임드쿼리_기반_조회_테스트() {
<!-- when : @NamedQuery에 정의된 정적 쿼리 실행 -->
List<엔티티클래스명> 결과리스트변수 = entityManager
.createNamedQuery("엔티티논리이름.조회쿼리이름", 엔티티클래스명.class)
.getResultList();
<!-- then : 조회 결과 검증 및 출력 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(System.out::println);
}
ORM의 기능을 이용하면서 JPA가 제공하는 JPQL이 아닌 SQL 문법을 그대로 쓰는 방식
복잡한 JOIN이나 서브쿼리 작성 시 JPQL보다 자유롭고 간단
@SqlResultSetMapping 엔티티 생성(결과 매핑 설정)
@Entity(name = "엔티티논리이름")
@Table(name = "테이블명")
<!-- Native SQL(순수 SQL) 을 사용할 때
결과를 엔티티 / 특정 컬럼값으로 매핑해주는 설정 -->
@SqlResultSetMappings(
value = {
<!-- 1. 자동 매핑 방식 - 엔티티 필드에 @Column이 명시되어 있을 때 자동으로 매핑됨 -->
@SqlResultSetMapping(
<!-- 결과 매핑 식별용 이름 -->
name = "결과매핑이름_자동",
entities = {
@EntityResult(entityClass = 엔티티클래스명.class)
},
columns = {
<!-- 기존 테이블에 없는 필드 추가 컬럼 -->
@ColumnResult(name = "추가조회컬럼명")
}
),
<!-- 2. 수동 매핑 방식 - 컬럼과 필드를 명시적으로 지정 -->
@SqlResultSetMapping(
name = "결과매핑이름_수동",
entities = {
@EntityResult(entityClass = 엔티티클래스명.class, fields = {
@FieldResult(name = "필드명1", column = "컬럼명1"),
@FieldResult(name = "필드명2", column = "컬럼명2"),
@FieldResult(name = "필드명3", column = "컬럼명3")
})
},
columns = {
@ColumnResult(name = "추가조회컬럼명")
}
)
}
)
public class 엔티티클래스명 {
@Id
@Column(name = "기본키컬럼명")
private 기본키자료형 기본키필드;
@Column(name = "일반컬럼명1")
private 자료형 필드1;
@Column(name = "일반컬럼명2")
private 자료형 필드2;
public 엔티티클래스명() {}
public 엔티티클래스명(기본키자료형 기본키필드, 자료형 필드1, 자료형 필드2) {
this.기본키필드 = 기본키필드;
this.필드1 = 필드1;
this.필드2 = 필드2;
}
<!-- Getter / Setter / toString() 생략 -->
}
테스트 코드 작성
public class NativeQueryTests {
private static EntityManagerFactory entityManagerFactory;
private static EntityManager entityManager;
@BeforeAll
public static void initFactory() {
<!-- persistence.xml의 persistence-unit 이름 사용 -->
entityManagerFactory = Persistence.createEntityManagerFactory("퍼시스턴스유닛이름");
}
@BeforeEach
public void initManager() {
entityManager = entityManagerFactory.createEntityManager();
}
@AfterAll
public static void closeFactory() {
entityManagerFactory.close();
}
@AfterEach
public void closeManager() {
entityManager.close();
}
<!-- 여러 테스트 -->
}
@Test
@DisplayName("Native Query - 결과 타입 정의 방식 테스트")
public void 네이티브쿼리_결과타입정의_테스트() {
<!-- given : 쿼리 조건에 사용할 파라미터 -->
기본키자료형 파라미터값 = 특정기본키;
<!-- when : 전체 컬럼을 조회하는 네이티브 쿼리 작성 -->
String query변수 = "SELECT 컬럼1, 컬럼2, 컬럼3, ... FROM 테이블명 WHERE 조건컬럼 = ?";
<!-- 일부 컬럼만 조회하는 것은 불가능 -->
<!-- setParameter(1, 파라미터값)에서 1은 ?에 들어갈 위치기반 파라미터
파라미터값은 해당 위치에 들어갈 실제 값 -->
Query 네이티브쿼리변수 = entityManager
.createNativeQuery(query변수, 엔티티클래스명.class)
.setParameter(1, 파라미터값);
엔티티클래스명 조회결과변수 = (엔티티클래스명) 네이티브쿼리.getSingleResult();
<!-- then : 결과 검증 -->
assertNotNull(조회결과);
<!-- 엔티티 매핑으로 인해 영속성 컨텍스트에 포함되어 있음 -->
assertTrue(entityManager.contains(조회결과변수));
System.out.println(조회결과변수);
}
@Test
@DisplayName("Native Query - 결과 타입 정의 불가능한 경우 테스트")
public void 네이티브쿼리_결과타입불특정_조회_테스트() {
<!-- when : 일부 컬럼만 조회하는 네이티브 쿼리 작성 (엔티티 전체 매핑 불가) -->
String query변수 = "SELECT 컬럼1, 컬럼2 FROM 테이블명";
<!-- createNativeQuery()의 두 번째 파라미터는
엔티티 클래스만 넣을 수 있도록 설계된 메서드
따라서 일부 컬럼만 조회할 경우 결과 타입을 지정하지 않고 사용해야 함
결과는 Object[] 형태로 반환 -->
List<Object[]> 결과리스트변수 = entityManager.createNativeQuery(query변수).getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(결과리스트변수);
결과리스트변수.forEach(행 -> {
Stream.of(행).forEach(열값 -> System.out.print(열값 + " "));
System.out.println();
});
}
<!-- Entity에 작성되어 있는 @SqlResultSetMapping의 name으로 연결 -->
@Test
@DisplayName("Native Query - 자동 결과 매핑 사용 테스트")
public void 네이티브쿼리_자동결과매핑_조회_테스트() {
<!-- when : 엔티티 + 추가 컬럼(menu_count)을 조회하는 네이티브 쿼리 작성 -->
String query변수 = "SELECT a.기본키컬럼, a.일반컬럼1, a.일반컬럼2, COALESCE(v.추가컬럼, 0) 추가컬럼 " +
"FROM 테이블명 a " +
"LEFT JOIN (SELECT COUNT(*) AS 추가컬럼, b.조인컬럼 " +
" FROM 조인테이블 b " +
" GROUP BY b.조인컬럼) v ON a.기본키컬럼 = v.조인컬럼 " +
"ORDER BY 1";
Query 네이티브쿼리변수 = entityManager.createNativeQuery(query변수, "결과매핑이름_자동");
List<Object[]> 결과리스트변수 = 네이티브쿼리변수.getResultList();
<!-- then : 엔티티 + 추가 컬럼 결과 검증 -->
assertNotNull(결과리스트변수);
<!-- 첫 번째 컬럼이 엔티티임을 확인 -->
assertTrue(entityManager.contains(결과리스트변수.get(0)[0]));
결과리스트변수.forEach(행 -> {
Stream.of(행).forEach(열값 -> System.out.print(열값 + " "));
System.out.println();
});
}
@Test
@DisplayName("Native Query - 수동 결과 매핑 사용 테스트")
public void 네이티브쿼리_수동결과매핑_조회_테스트() {
<!-- when : 수동 매핑을 위한 네이티브 SQL 작성 -->
String query변수 = "SELECT " +
" a.기본키컬럼, a.일반컬럼1, a.일반컬럼2, COALESCE(v.추가컬럼, 0) 추가컬럼 " +
" FROM 테이블명 a " +
" LEFT JOIN (SELECT COUNT(*) AS 추가컬럼, b.조인컬럼 " +
" FROM 조인테이블 b " +
" GROUP BY b.조인컬럼) v ON (a.기본키컬럼 = v.조인컬럼) " +
" ORDER BY 1";
<!-- 결과 매핑 이름("categoryCountManualMapping")은 @SqlResultSetMapping 어노테이션에서 정의한 이름이어야 함 -->
Query 네이티브쿼리변수 = entityManager.createNativeQuery(query변수, "결과매핑이름_수동");
List<Object[]> 결과리스트변수 = 네이티브쿼리변수.getResultList();
<!-- then : 엔티티 + 추가 컬럼 확인 -->
assertNotNull(결과리스트변수);
<!-- 첫 컬럼이 엔티티인지 확인 -->
assertTrue(entityManager.contains(결과리스트변수.get(0)[0]));
결과리스트변수.forEach(행 -> {
Stream.of(행).forEach(열값 -> System.out.print(열값 + " "));
System.out.println();
});
}
네이티브 SQL도 JPQL처럼 @NamedNativeQuery를 사용해 정적 SQL을 정의하고 재사용할 수 있다
Entity 생성
@Entity(name = "엔티티논리이름")
@Table(name = "테이블명")
<!-- 결과 매핑 정의 (자동 매핑 방식 + 추가 컬럼 포함 시) -->
@SqlResultSetMapping(
<!-- resultSetMapping에 사용할 이름 -->
name = "엔티티논리이름.쿼리이름",
entities = {
@EntityResult(entityClass = 엔티티클래스명.class)
},
columns = {
@ColumnResult(name = "추가조회컬럼명")
}
)
<!-- 네이티브 쿼리 정의 (매핑과 함께 사용) -->
@NamedNativeQueries(
<!-- 쿼리가 한 개일 경우 value 생략 가능 -->
value = {
@NamedNativeQuery(
<!-- resultSetMapping에 사용할 이름 -->
name = "결과매핑이름",
query = "SELECT a.컬럼1, a.컬럼2, a.컬럼3, COALESCE(v.추가컬럼, 0) 추가컬럼명 " +
"FROM 테이블명 a " +
"LEFT JOIN (SELECT COUNT(*) AS 추가컬럼, b.조인컬럼 " +
" FROM 조인테이블명 b " +
" GROUP BY b.조인컬럼) v ON a.기본키컬럼 = v.조인컬럼 " +
"ORDER BY 1",
resultSetMapping = "엔티티논리이름.쿼리이름"
)
}
)
public class 엔티티클래스명 {
@Id
@Column(name = "기본키컬럼")
private 기본키타입 기본키필드;
@Column(name = "컬럼명1")
private 자료형 필드1;
@Column(name = "컬럼명2")
private 자료형 필드2;
public 엔티티클래스명() {}
public 엔티티클래스명(기본키타입 기본키필드, 자료형 필드1, 자료형 필드2) {
this.기본키필드 = 기본키필드;
this.필드1 = 필드1;
this.필드2 = 필드2;
}
<!-- Getter / Setter / toString() 생략 -->
}
테스트 코드
public class NamedNativeQueryTests {
private static EntityManagerFactory entityManagerFactory;
private static EntityManager entityManager;
@BeforeAll
public static void initFactory() {
<!-- persistence.xml의 persistence-unit 이름 사용 -->
entityManagerFactory = Persistence.createEntityManagerFactory("퍼시스턴스유닛이름");
}
@BeforeEach
public void initManager() {
entityManager = entityManagerFactory.createEntityManager();
}
@AfterAll
public static void closeFactory() {
entityManagerFactory.close();
}
@AfterEach
public void closeManager() {
entityManager.close();
}
<!-- 여러 테스트 -->
}
@Test
@DisplayName("Named Native Query를 이용한 정적 SQL 결과 조회 테스트")
public void 네임드네이티브쿼리_조회_테스트() {
<!-- when : @NamedNativeQuery로 정의한 쿼리 호출 (쿼리 이름으로 조회 실행) -->
Query 네이티브쿼리 = entityManager.createNamedQuery("엔티티논리이름.쿼리이름");
List<Object[]> 결과리스트변수 = 네이티브쿼리.getResultList();
<!-- then : 결과 검증 및 출력 -->
assertNotNull(결과리스트변수);
<!-- 첫 컬럼이 엔티티인지 확인 -->
assertTrue(entityManager.contains(결과리스트변수.get(0)[0]));
결과리스트변수.forEach(행 -> {
Stream.of(행).forEach(열값 -> System.out.print(열값 + " "));
System.out.println();
});
}

연관관계를 어떻게 설정하는가? -> 기획 의도에 따라 달라진다

String vs StringBuilder 차이점
String