[JPA] JPQL

olsohee·2023년 8월 1일
0

JPA

목록 보기
10/21

1. JPQL이란?

JPA는 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다. JPA가 지원하는 검색 방법은 다음과 같다.

  • JPQL(Java Persistence Query Language)

  • Criteria 쿼리: JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음

  • 네이티브 SQL: JPA에서 JPQL 대신 직접 SQL을 작성할 수 있다.

  • QueryDSL: Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음

  • JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용: 필요에 따라 JDBC를 직접 사용할 수 있다.

이 중 가장 기본이 되는 JPQL에 대해 알아보자. JPQL의 특징은 다음과 같다.

  • 테이블이 아닌 엔티티 객체를 대상으로 쿼리하는 객체지향 쿼리이다.

  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다. 따라서 데이터베이스 방언(dialect)만 변경하면 JPQL을 수정하지 않아도 데이터베이스 변경이 가능하다.

  • JPA는 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한다. 그리고 조회한 결과로 엔티티 객체를 생성해서 반환한다.


Select문

Select문은 다음과 같이 사용한다.

select m from Member as m where m.username = 'member1'
  • 엔티티(Member)와 속성(username)은 대소문자를 구분한다. 반면 select, from, as와 같은 JPQL 키워드는 대소문자를 구분하지 않는다.

  • JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다. 엔티티 명은 @Entity(name='xxx')로 지정할 수 있으며, 지정하지 않으면 클래스 명이 엔티티 명이 된다.

  • Member as m 부분을 보면 Memberm이라는 별칭(식별 변수: Identification variable)을 주었다. JPQL은 별칭을 필수로 사용해야 한다. 참고로 as는 생략할 수 있따.


2. TypeQuery, Query

  • TypeQuery: 반환 타입이 명확할 때 사용한다.

  • Query: 반환 타입이 명확하지 않을 때 사용한다.

TypedQuery<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 = 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]);
}
  • 위 예제의 경우, 조회 대상이 회원 이름(String)과 나이(Integer)이므로 조회 대상 타입이 명확하지 않다. 이처럼 select 절에서 여러 엔티티나 칼럼을 조회할 때는 반환할 타입이 명확하지 않으므로 Query를 사용해야 한다.

  • Query 객체는 조회 대상이 둘 이상이면 Object[]를 반환하고, 조회 대상이 하나이면 Object를 반환한다. 예를 들어 select m.username from Member m이면 결과를 Object로 반환하고, select m.username, m.age from Member m이면 Object[]를 반환한다.


3. 결과 조회

  • query.getResultList(): 결과가 하나 이상일 때 리스트 형태로 반환한다.

    • 만약 결과가 없으면 빈 리스트를 반환한다.
  • query.getSingleResult(): 결과가 하나일 때 단일 객체를 반환한다.

    • 만약 결과가 없으면 NoResultException 예외가 발생한다(spring data jpa에서는 null이나 optional을 반환한다).

    • 반면 결과가 둘 이상이면 NonUniqueResultException 예외가 발생한다. 즉 getSingleResult()는 결과가 정확히 하나일 때가 아니면 예외가 발생한다.


4. 파라미터 바인딩

JPQL은 위치 기준 파라미터 바인딩과 이름 기준 파라미터 바인딩 방식을 지원한다. 파라미터가 중간에 추가될 것을 고려해 위치 기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 안전하고 명확하다.

  • 이름 기준 파라미터 바인딩: 앞에 :를 사용한다.
Member findMember = em.createQuery("select m from Member m where m.username=:username", Member.class)
					.setParameter("username", "member1")
					.getSingleResult();
  • 위치 기준 파라미터 바인딩: ? 뒤에 위치 값을 주면 된다. 위치 값은 1부터 시작한다.
Member findMember = em.createQuery("select m from Member m where m.username=?1", Member.class)
					.setParameter(1, "member1")
             	    .getSingleResult();

SQL Injection 공격을 방어하는 파라미터 바인딩 방식

파라미터 바인딩 방식을 사용하지 않고 다음과 같이 직접 문자를 더해 JPQL을 만들면 SQL Injection 공격을 당할 수 있다.

"select m from Member m where m.username = '" + usernameParam + "'"

따라서 파라미터 바인딩 방식은 선택이 아닌 필수이다.

em.createQuery("select m from Member m where m.username = :username", Member.class)
	.setParameter("username", usernameParam)
    .getResultList();

5. 프로젝션

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

엔티티 프로젝션

select m from Member m // 회원 조회
select m.team from Member m // 팀 조회
  • 회원과 팀이라는 엔티티를 대상으로 조회한다.

  • 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

임베디드 타입 프로젝션

  • 임베디드 타입은 조회의 시작점이 될 수 없다. 따라서 다음과 같이 임베디드 타입인 Address를 조회의 시작점으로 사용할 수 없다.
String query = "select a from Address a";
  • 임베디드 타입을 조회하려면 다음과 같이 엔티티를 통해서 임베디드 타입을 조회해야 한다.
String query = "select o.address from Order o";
List<Address> addresses = em.createQuery(query, Address.class)
							.getResultList();
  • 임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.

스칼라 타입 프로젝션

  • 숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다.
List<String> usernames = em.createQuery("select m.username from Member m", String.class)
							.getResultList();
  • 중복 데이터를 제거하려면 distinct를 사용한다.
select distinct m.username from Member m

여러 값 조회

  • 하나의 엔티티를 대상으로 조회하는 것이 아닌, 프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 Query를 사용해야 한다.
List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m")
							.getResultList();
                            
for(Object[] row : resultList) {
	String username = (String)row[0];
    Integer age = (Integer)row[1];
}
  • 스칼라 타입 뿐만 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있다. 그리고 이 경우에도 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
List<Object[]> resultList = em.createQuery("select o.member, o.product, o.orderAmount from Order o")
							.getResultList();
                            
for(Object[] row : resultList) {
	Member member = (Member)row[0]; // 엔티티
	Product product = (Product)row[1]; // 엔티티
    int orderAmount = (Integer)row[2]; // 스칼라
}

new 명령어

  • new 명령어를 사용하면 조회한 값들을 이용해서 dto로 손쉽게 변환할 수 있다.
TypeQuery<UserDto> query = 
		em.createQuery("select new jpabook.jpql.UserDto(m.username, m.age) from Member m", 	
        UserDto.class);

List<UserDto> resultList = query.getResultList();
  • 이때 UserDto와 같은 클래스명은 패키지 명을 포함하여 작성해주어야 하고, 순서와 타입이 일치하는 생성자가 있어야 한다.
public class UserDto {
    
    private String username;
    private int age;

    public UserDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

6. 페이징

페이징 처리용 SQL을 작성하는 일은 지루하고 반복적이며, 무엇보다 데이터베이스마다 페이징을 처리하는 SQL 문법이 다르다. JPA는 페이징을 다음 두 API로 추상화했다. 따라서 데이터베이스 방언에 따라 JPQL이 적절한 SQL문으로 변환된다.

  • setFirstResult(int startPosition): 조회 시작 위치(0부터 시작)

  • setMaxResults(int maxResult): 조회할 데이터 수

TypedQuery<Member> query = em.createQuery("select m from Member order by m.age desc", Member.class);

query.setFirstResult(0);
query.setMaxResults(10);
query.getResultList();

7. 집합과 정렬

집합 함수

집합 함수 사용 예는 다음과 같다.

select count(m),   // 회원수
	   sum(m.age), // 나이 합
       avg(m.age), // 평균 나이
       max(m.age), // 최대 나이
       min(m.age)  // 최소 나이
from Member m 

집합 함수 사용시 주의사항은 다음과 같다.

  • null 값은 무시되므로 통계에 잡히지 않는다.

  • 값이 없는데 sum, avg, max, min 함수를 사용하면 null 값이 된다. 단 count는 0이 된다.

  • distinct를 집합 함수 안에서 사용해서 중복된 값을 제거하고 집합을 구할 수 있다.
    ex, select count(distinct m.age) from Member m

  • distictcount에서 사용할 때 임베디드 타입은 지원하지 않는다.

group by, having

  • group by는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다.
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
  • havinggroup by와 함께 사용하는데, group by로 그룹화한 통계 데이터를 기준으로 필터링한다.
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 // 평균나이가 10살 이상인 그룹을 조회

정렬(order by)

  • order by는 결과를 정렬할 때 사용한다.

  • asc: 오름차순(기본값)

  • desc: 내림차순

select m from Member m order by m.age desc, m.username asc
// 나이를 기준으로 내림차순 정렬, 같으면 이름을 기준으로 오름차순 정렬

8. 조인

내부 조인

회원과 팀을 내부 조인해서 'Team A'에 소속된 회원을 조회하는 JPQL은 다음과 같다.

String teamName = "teamA";
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의 조인과 다르게 JPQL의 조인은 연관 필드를 사용한다. 위 예제에서는 m.team이 연관 필드이다. 그리고 이 연관 필드에 t라는 별칭을 주었다.

외부 조인

JPQL의 외부 조인은 다음과 같이 사용한다.

select m from Member m left join m.team t

컬렉션 조인

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

  • 회원 -> 팀으로 조인하는 것은 다대일 조인이면서 단일 값 연관 필드(m.team)를 사용한다.

  • 팀 -> 회원으로 조인하는 것은 일대다 조인이면서 컬렉션 값 연관 필드(t.members)를 사용한다.

예를 들어 다음 코드는 팀과 팀이 보유한 회원들을 컬렉션 값 연관 필드로 외부 조인한다.

select t, m from Team t left join t.members m

세타 조인

where 절을 사용해서 세타 조인을 할 수 있다. 세타 조인을 사용하면 전혀 관계없는 엔티티도 조인할 수 있다. 다음 예제는 전혀 관련없는 Member.username과 Team.name을 조인한다.

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

세타 조인은 내부 조인만 지원한다. 반면 다음에 나오는 join on 절을 사용하면 관계없는 엔티티 간에 외부 조인도 할 수 있다.

join on 절

on 절을 사용하면 조인 대상을 필터링해서 조인할 수 있다. 그리고 세타 조인과 다르게 연관관계 없는 엔티티 간에 외부 조인도 가능하다.

다음 예제는 회원과 팀을 외부 조인하면서 팀 이름이 'teamA'인 팀만 조인한다.

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

다음 예제는 연관관계가 없는 Member.username과 Team.name을 외부 조인한다.

select m, t from Member m left join Team t on m.username = t.name
profile
공부한 것들을 기록합니다.

1개의 댓글

comment-user-thumbnail
2023년 8월 1일

감사합니다. 이런 정보를 나눠주셔서 좋아요.

답글 달기