JPA 4

j0yy00n0·2025년 4월 17일
post-thumbnail

2025.03.27

JPA

JPQL

JPA의 가장 큰 한계 중 하나는 SQL의 SELECT 구문처럼 복잡한 조회 쿼리를 자유롭게 작성하기 어렵다.
JPA는 객체 중심 매핑을 제공하지만, 기본적으로는 단순한 CRUD 중심의 API를 제공하기 때문에 복잡한 조건 검색이나 조인을 표현하기엔 한계가 있다.
-> JPQL(Java Persistence Query Language) 해결

JPQL(Java Persistence Query Language)

  • JPQL은 엔터티 객체를 중심으로 개발할 수 있는 객체 지향 쿼리이다.
    -> 복잡한 조건, 조인, 정렬, 집계 등의 쿼리를 객체지향적으로 표현
  • SQL보다 간결하며 특정 DBMS에 의존하지 않는다.
  • 방언을 통해 해당 DBMS에 맞는 SQL을 실행하게 된다.
  • JPQL은 find() 메소드를 통한 조회와 다르게 항상 데이터베이스에 SQL을 실행해서 결과를 조회한다.

JPQL의 기본 문법

  • SELECT, UPDATE, DELETE 등의 키워드 사용은 SQL과 동일
  • INSERT 는 persist() 메소드를 사용
  • 키워드는 대소문자를 구분하지 않지만, 엔터티와 속성은 대소문자를 구분함에 유의
  • 엔터티 별칭을 필수로 사용, 별칭 없이 작성하면 에러 발생

JPQL 사용 방법

  1. 작성한 JPQL(문자열)을 entityManager.createQuery()메소드를 통해 쿼리 객체로 만든다.
  2. 쿼리 객체는 TypedQuery, Query 두 가지가 있다.
    • TypedQuery : 반환할 타입을 명확하게 지정하는 방식일 때 사용하며 쿼리 객체의 메소드 실행 결과로 지정한 타입이 반환 된다.
    • Query : 반환할 타입을 명확하게 지정할 수 없을 때 사용하며 쿼리 객체 메소드의 실행 결과로 Object 또는 Object[]이 반환 된다.
  3. 쿼리 객체에서 제공하는 메소드 getSingleResult() 또는 getResultList()를 호출해서 쿼리를 실행하고 데이터베이스를 조회한다.
    • getSingleResult() : 결과가 정확히 한 행일경우 사용하며 없거나 많으면 예외가 발생한다.
    • getResultList() : 결과가 2행 이상일 경우 사용하며 컬렉션을 반환한다. 결과가 없으면 빈 컬렉션을 반환한다.

기본 JPQL 생성

  • persistence.xml 설정
<?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 생성
@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 생성

JPQL에서 조건절에 고정값 대신 변수(파라미터) 를 사용하여 동적으로 값을 주입
-> 사용자 입력 값이나 조건에 따라 유연한 쿼리 구성 가능

  1. 이름 기준 파라미터(named parameters)
    • ':' 다음에 이름 기준 파라미터를 지정한다.
    • setParameter("파라미터이름", 파라미터변수)
  2. 위치 기준 파라미터(positional parameters)
    • '?' 다음에 값을 주고 위치 값은 1부터 시작한다.
    • setParameter(1, 파라미터변수)
  • Entity 생성
  • 테스트 코드 작성
    @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);
    }

JPQL 프로젝션(projection)

SELECT 절에 조회할 대상을 지정하는 것

  • SELECT {프로젝션 대상} FROM
  1. 엔티티 프로젝션
    • SELECT 별칭 FROM 엔티티명 별칭
    • 원하는 객체를 바로 조회할 수 있다.
    • 조회된 엔티티는 영속성 컨텍스트가 관리한다.
    • entityManager.find() 와 유사한 동작 -> PK기반 단건 조회 전용
      jpal은 다양한 조건 조회용
    @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);
    }
  1. 임베디드 타입 프로젝션
    • @Embeddable 타입 필드 조회
    • 엔티티 내부의 복합 필드를 조회할 수 있지만 FROM 절에는 직접 사용할 수 없음
      -> 조회의 시작점이 아니다.
    • 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.
    • 단순한 값 객체 역할
    @Test
    public void 임베디드_타입_프로젝션_테스트() {

        <!-- when : 임베디드 타입 필드만 조회하는 JPQL 작성 -->
        String jpql변수 = "SELECT 별칭.임베디드필드 FROM 엔티티논리이름 별칭";
        List<임베디드클래스> 임베디드객체리스트변수 =
                entityManager.createQuery(jpql변수, 임베디드클래스.class).getResultList();

        <!-- then : 결과 검증 및 출력 -->
        assertNotNull(임베디드객체리스트변수);
        임베디드객체리스트변수.forEach(System.out::println);
    }
  1. 스칼라 타입 프로젝션
    • 숫자, 문자, 날짜 같은 기본 타입 필드조회
    • 엔티티가 아닌 필드 단위 조회
    • 스칼라 타입은 영속성 컨텍스트에서 관리되지 않는다.
    • 결과가 Object[], List<Object[]> 형태로 반환
    @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);
        });
    }
  1. new 명령어를 활용한 프로젝션
    • 다양한 종류의 단순 값들을 DTO로 바로 조회하는 방식
    • DTO는 생성자 기반으로 값을 전달받아야 함
    • new 패키지명.DTO명을 쓰면 해당 DTO로 바로 반환받을 수 있다.
    • new 명령어를 사용한 클래스의 객체는 엔티티가 아니므로 영속성 컨텍스트에서 관리되지 않는다.
    @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);
    }

JPQL 페이징(Paging) - 순수 JPA 기준

JPA에서는 대량의 데이터를 조회할 때 페이지 단위로 나누어 조회하는 기능을 제공

  • 각 DBMS 마다 페이징 SQL 문법이 다르다
    ->JPA는 페이징 기능을 공통 API로 추상화하여 제공한다.
  • setFirstResult(int offset) : 조회 시작 위치 (0부터 시작)
  • setMaxResults(int limit) : 한 번에 조회할 행 수
    Pageable – Spring Data JPA 기준
    Pageable은 Spring Data JPA에서 제공하는 페이징 기능을 위한 인터페이스
  • 데이터를 "몇 번째 페이지", "몇 개씩", "어떻게 정렬할지" 설정 가능
  • Pageable → 페이징 요청 정보를 담는 객체 (페이지 번호, 사이즈, 정렬 기준 등)
  • Page< T > → 페이징 응답 결과 정보를 담는 객체
    (조회된 데이터 + 전체 개수 + 페이지 수)
    @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);
    }

JPQL 그룹함수

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("조회된 개수 = " + 개수조회결과변수);
    }
  1. null 반환 여부
    • 조회 대상이 없는 경우 : count는 0,
      나머지(MAX, MIN, SUM, AVG)는 null 반환
    • 기본 자료형으로 받을 경우 -> NullPointerException 발생 위험
    @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 반환됨
        });
    }
  1. HAVING 절에서 사용하는 파라미터
    • 그룹 함수 결과는 Long 또는 Double 타입이므로
      HAVING 절에 바인딩할 파라미터도 같은 타입으로 맞춰야 함
    @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);
        });
    }

JPQL 조인(JOIN)

일반 조인

  • SQL의 기본 조인 방식과 동일 (내부, 외부, 컬렉션, 세타 조인 등)
  • 연관 엔티티가 아닌 연관 엔티티의 특정 컬럼이나 필드, 즉 join 대상의 일부 값만 조회
  • 지연 로딩(LAZY)이 기본
    페치 조인 (Fetch Join)
  • 성능 최적화를 위한 JPQL 고유 기능
  • 연관 엔티티까지 함께 즉시 로딩(EAGER) 수행
  • join fetch 문법 사용
  • N+1 문제 해결용으로 많이 쓴다.
  • 연관된 모든 엔티티를 한 번의 쿼리로 가져올 수 있다
    @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);
    }

JPQL 서브쿼리

select, from절에서는 사용할 수 없고 where, having절에서만 사용이 가능
JPQL 서브쿼리 한계
1. 가독성 저하
- 복잡한 쿼리일수록 가독성이 급격히 떨어짐
- 서브쿼리/동적 쿼리 구성 시 문자열 처리가 복잡해짐
2. 문자열 기반으로 -> 컴파일 시 문법 오류 확인 불가
- 쿼리는 문자열로 작성되므로 컴파일 단계에서는 문법 검증이 되지 않는다.
- 런타임 시점에만 오류 발생한다. -> SQLSyntaxErrorException
3. 띄어쓰기 / 구문 오류 민감
- 작은 오타, 공백 오류 등으로 쉽게 에러 발생
QueryDSL 한계 해결

  • 타입 안전 : 쿼리를 코드로 작성 -> 컴파일 시점에 문법 오류 확인 가능
  • 코드 자동완성 : IDE의 도움을 받아 필드명/조건 등을 자동 완성 가능
  • 동적 쿼리 : 조건에 따라 쿼리 구성 변경이 쉬움
  • 복잡한 조인 : 다양한 연관관계를 깔끔하게 표현 가능
    QueryDSL 도입 시점
  • 동적 조건이 많은 복잡한 검색 로직이 필요할 때
  • 복잡한 다중 조인이나 서브쿼리를 자주 사용하는 경우
  • JPQL로는 유지보수가 어렵고 에러가 자주 나는 경우
    @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);
    }

JPQL 동적 정적 쿼리

동적 쿼리 (Dynamic Query)

  • EntityManager를 통해 문자열로 직접 작성하는 방식
  • 런타임 시점에 조건문, 반복문 등을 이용해 쿼리 문자열을 조합
  • createQuery("JPQL") 형태로 작성
  • 유연하지만 가독성 저하, 문법 오류는 컴파일 시점에 알 수 없음

정적 쿼리 (Static Query) - NamedQuery

  • 고정된 쿼리를 미리 정의해서 사용하는 방식
  • 미리 정의한 코드는 이름을 부여해서 사용
  • 엔티티 클래스에 @NamedQuery 어노테이션으로 등록
  • 코드의 재사용성과 유지보수성이 좋음
  • 컴파일 시점에 문법 오류를 잡을 수 있음
    JPA + mybatis 추천
    엔티티 클래스
@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);
    }

네이티브 쿼리 (Native query)

ORM의 기능을 이용하면서 JPA가 제공하는 JPQL이 아닌 SQL 문법을 그대로 쓰는 방식

Native query

복잡한 JOIN이나 서브쿼리 작성 시 JPQL보다 자유롭고 간단

  • DB 성능 최적화 쿼리(Hint, Index 등)를 쓰고 싶을 때 유용
  • 복잡한 SQL을 그대로 사용하고 싶을 때 사용
  • 특정 DB(MySQL, Oracle 등)만 지원하는 SQL 문법을 써야 할 때 사용
  1. 결과 타입 정의 : 전체 컬럼을 조회해서 특정 클래스에 매핑할 때
    • public Query createNativeQuery(String sqlString, Class resultClass);
    • createNativeQuery(SQL, 클래스.class)
  2. 결과 타입 없음 : 일부 컬럼만 조회할 때 (결과는 Object[])
    • public Query createNativeQuery(String sqlString);
    • createNativeQuery(SQL)
  3. 결과 매핑 사용 : 복잡한 결과를 수동으로 매핑하고 싶을 때
    • public Query createNativeQuery(String sqlString, String resultSetMapping);
    • createNativeQuery(SQL, 매핑이름)

@SqlResultSetMapping 엔티티 생성(결과 매핑 설정)

  • @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();
    }
    
    <!-- 여러 테스트 -->
}
    1. 결과 타입 정의
    @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(조회결과변수);
    }
    1. 결과 타입을 정의할 수 없을 때
    @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();
        });
    }
    1. 결과 매핑 사용
<!-- 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();
        });
    }

NamedNativeQuery

네이티브 SQL도 JPQL처럼 @NamedNativeQuery를 사용해 정적 SQL을 정의하고 재사용할 수 있다

  • 복잡한 SQL을 문자열로 직접 작성하지만, 이름을 지정해 정적 쿼리로 재사용 가능
  • 런타임 시 쿼리 문자열을 작성할 필요 없이, 이름으로 쿼리 호출
  • 복잡한 JOIN, 집계 쿼리, 또는 DB 특화 SQL을 자주 재사용할 때 유리
  • resultSetMapping을 함께 사용해 엔티티 매핑 또는 수동 매핑 가능

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();
        });
    }

JPA 핵심

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

  • 로딩 전략을 어떻게 가져가는가?
  • JPA 가진 단점인 조회 시 처리를 어떻게 하는가?
  • 복잡한 쿼리문이나 동적 쿼리문을 어떻게 처리하는가?
  • 영속성 컨텍스트에서 관리가 되는가?

참고

String vs StringBuilder 차이점
String

  • 불변(immutable) 객체
  • 문자열 변경 시마다 새로운 객체 생성
  • 느림 (문자열 연산 많으면 성능 저하)
  • 단순 문자열, 메시지 표시 등
  • 문자열을 한두 번만 붙이는 경우 사용
    StringBuilder
  • 가변(mutable) 객체
  • 하나의 객체에 내용을 계속 추가/변경
  • 빠름 (문자열 조합, 반복 처리에 효율적)
  • 반복적인 문자열 연결/가공 시
  • 문자열을 반복해서 조합/변경해야 하는 경우 사용
profile
잔디 속 새싹 하나

0개의 댓글