JPA 톺아보기 - JPQL

Janek·2023년 2월 24일
0

JPA 톺아보기

목록 보기
9/10
post-thumbnail

해당 포스팅은 인프런에서 제공하는 김영한 님의 '자바 ORM 표준 JPA 프로그래밍 - 기본편'을 수강한 후 정리한 글입니다. 유료 강의를 정리한 내용이기에 제공되는 예제나 몇몇 내용들은 제외하였고, 정리한 내용을 바탕으로 글 작성자인 저의 언어로 다시 작성한 글이기에 서술이 부족하거나 잘못된 내용이 있을 수 있습니다. 그렇기에 해당 글은 개념에 대한 참고 정도만 해주시고, 강의를 통해 학습하시기를 추천합니다.

JPA는 엔티티 객체를 중심으로 개발이 가능하게 도와준다. 검색 쿼리 또한 테이블이 아닌 엔티티 객체를 대상으로 검색을 하는데 현실적으로 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능하다. 때문에 필요한 데이터만 DB에서 불러오기 위해서는 조건이 포함된 SQL이 필요하다.

JPQL

JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공한다. SQL 문법과 유사(SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN)하며, 데이터베이스 테이블을 대상으로 쿼리를 보내는 SQL과 달리 객체를 대상으로 쿼리를 보낸다. 또한 SQL을 추상화해서 사용하기 때문에 특정 데이터베이스의 SQL 문법에 의존적이지 않다.

JPQL 문법

SELECT m FROM Member m WHERE m.age > 20과 같이 엔티티와 속성은 대소문자를 구분하며, JPQL 키워드는 구분하지 않는다. 또한 객체를 대상으로 검색하기 때문에 테이블명이 아닌 엔티티 객체의 이름을 사용해야하며, 별칭(Alias, as는 생략 가능)을 반드시 명시해야 한다.

기본적으로 COUNT, SUM, AVG, MAX, MIN과 같은 집합 함수나 GROUP BY, HAVING과 같은 집합, ORDER BY와 같은 정렬 조건도 지원한다.(UNION은 지원하지 않는다.)

TypeQuery, Query

TypeQuery 객체는 반환 타입이 명확할 때 사용하며 TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class)과 같이 제네릭으로 타입을 명시해줄 수 있다. 반면 반환 타입을 명확하게 지정할 수 없는 경우 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; // 결과가 둘 이상이면 배열 반환
    System.out.println("username = " + result[0]);
    System.out.println("age = " + result[1]);
}

결과 조회

다음 메서드들을 호출하면 실제 쿼리를 실행해서 데이터베이스를 조회한다.

  • query.getResultList();
    • 결과를 컬렉션으로 반환
    • 결과가 없으면 빈 컬렉션을 반환한다.
  • query.getSingleResult();
    • 결과가 정확히 하나일 때 사용한다.
    • 결과가 없으면 NoResultException 예외가 발생
    • 결과가 한개보다 많으면 NonUniqueResultException 예외가 발생한다. 따라서 사용에 주의해야 한다.

파라미터 파인딩

JDBC는 위치 기준 파라미터 파인딩만을 지원하지만(NamedParameterJdbcTemplate을 통해 지원한다.) JPQL은 이름 기준 파라미터 바인딩도 지원한다.

이름 기준 파라미터 바운딩

이름 기준 파라미터 바운딩은 기준 파라미터 앞에 :를 붙여서 사용한다.

List<Member> members = em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class)
                          .setParameter("username", "test")
                          .getResultList();

위치 기준 파라미터 바인딩

위치 기준으로 사용하려면 ?1과 같이 ? 뒤에 위치 값을 지정하고 파라미터 값을 넣어주면 된다. 위치 값은 1부터 시작된다.

프로젝션

SELECT 절에 조회할 대상을 지정하는 것을 프로젝션(projection)이라 한다. 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 있다.

SELECT {프로젝션 대상} FROM ...

엔티티 프로젝션

SELECT m FROM Member m		// 회원
SELECT m.team FROM Member m	// 팀

위 예시에서 프로젝션은 모두 엔티티이다. 컬럼을 나열해서 조회해야 하는 SQL과 달리 객체 단위로 바로 조회가 가능하다. 이렇게 조회된 엔티티는 모두 영속성 컨텍스트에 의해서 관리된다.

임베디드 타입 프로젝션

임베디드 타입은 엔티티와 거의 비슷하게 사용된다. 그러나 조회의 시작점이 될 수 없다는 제약이 있기 때문에 엔티티를 통해 임베디드 타입을 조회해야 한다.

// Address = 임베디드 타입
SELECT a FROM Address a			// 조회 불가
SELECT m.address FROM Member m	// 엔티티를 통해 조회 가능

임베디드 타입은 엔티티 타입이 아닌 값 타입이기 때문에 직접 조회한 임베디드 타입은 영속성 컨텍스트의 관리 대상이 되지 않는다.

new 명령어

엔티티 객체나 임베디드 타입이 아닌 필드를 프로젝션할 경우 타입을 지정할 수 없어 TypeQuery를 사용할 수 없다. 이후 Object로 반환 받은 값을 DTO로 변환해서 사용하게 되는데, new 명령어를 통해 DTO 객체를 타입으로 지정하고 TypeQuery를 사용할 수 있다.

List<MemberDTO> memberList = em.createQuery(
		"SELECT new 패키지경로.MemberDTO(m.username, m.age) FROM Member m",
        MemberDTO.class
	)
    .getResultList();

new 명령어 사용시 패키지 명을 포함한 전체 클래스 명을 입력해야 하며, 순서와 타입이 일치하는 생성자가 필요하다.

페이징 API

JAP는 페이징을 setFirstResult(int startPosition);setMaxResults(int maxResult) 두 API로 추상화하였다. FirstResult의 시작은 0이다.

집합과 정렬

일반적으로 사용되는 COUNT, SUM, AVG, MAX, MIN과 같은 집합 함수를 모두 사용할 수 있다. 다음은 집합 함수 사용 시 참고사항을 정리한 것이다.

  • NULL 값은 무시하기 때문에 통계에 잡히지 않는다. DISTINCT가 정의되어 있어도 무시된다.
  • 값이 없을 때 사용하면 COUNT를 제외한 함수는 NULL을, COUNT는 0을 반환한다.
  • DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거한 후 집합을 구할 수 있다.
    • SELECT COUNT(DISTINCT m.idx) FROM Member m
  • DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.

GROUP BY와 HAVING, ORDER BY 모두 일반적인 SQL과 동일하게 사용할 수 있다.

profile
만들고 나누며, 세상을 이롭게 하고 싶습니다.

0개의 댓글