해당 글은 김영한 님의 자바 ORM 표준 JPA 프로그래밍을 참고하여 작성한 글입니다.
✅ JPQL이란?
JPQL(Java Persistence Query Language): 엔티티 객체를 조회하기 위해 설계된 객체 지향 쿼리 언어
▶ 특징
- JPQL은 데이터베이스의 테이블이 아닌 엔티티 객체를 대상으로 하는 쿼리 언어이다.
- SQL을 추상화했기 때문에 특정 데이터베이스 시스템에 의존하지 않는다.
- SQL과 유사한 문법을 가지며, JPQL은 내부적으로 SQL로 변환되어 실행된다.
▶ 기본 문법
select_문 ::=
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 ::= update_절 [where_절]
delete_문 ::= delete_절 [where_절]
▶ SQL과 다른 특징
select m
from Member as m
where m.age > 18
- 엔티티와 속성은 대소문자를 구분한다.
- 예: Member(엔티티), age(속성)은 대소문자를 구분해 줘야 한다.
- JPQL 키워드는 대소문자를 구분하지 않아도 된다.
- 예: SELECT, FROM, WHERE 같은 JPQL 키워드는 대소문자를 구분하지 않아도 된다.
- 엔티티 이름을 사용해야 한다.
- 예: Member는 클래스가 아니라 엔티티 이름이다.
- 엔티티 이름은 @Entity(name="Member")로 지정할 수 있으며, 별도로 지정하지 않으면 클래스 이름이 기본값으로 사용된다.
- 별칭은 필수이다.
- 예: Member as m에서 Member에 m이라는 별칭을 부여했다.
- as 키워드는 생략할 수 있다.
▶ 집합과 정렬
관계형 데이터베이스의 집합과 정렬 등의 함수를 제공한다.
select
COUNT(m), //회원 수
SUM(m.age), //나이 합
AVG(m.age), //평균 나이
MAX(m.age), //최대 나이
MIN(m.age) //최소 나이
from Member m
- GROUP BY, HAVING, ORDER BY 등 모두 사용이 가능하다.
▶ 문제점
- JPQL은 기본 문자열로 작성되기 때문에 컴파일 시 에러를 잡아내지 않는다. 따라서 문제가 있음에도 불구하고 정상적으로 작동하여 배포 시 문제가 발생할 수 있다.
- 동적으로 쿼리를 작성할 때 비효율적이다. (예: 특정 조건에 따라 쿼리를 다르게 작성해야 하는 경우 등)
✅ TypeQuery, Query
▶ TypeQuery
반환 타입이 명확할 때 사용한다.
🔽 예시 코드
//TypedQuery
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class);
- 반환 타입이 명확할 경우, 두 번째 파라미터에 타입 정보를 작성할 수 있다.
- 기본적으로 엔티티 타입을 기입한다.
- query2와 같이 string인 username의 정보는 String class 타입 정보를 기입할 수 있다.
▶ Query
반환 타입이 명확하지 않을 때 사용한다.
🔽 예시 코드
//Query
Query query = em.createQuery("select m.username, m.age from Member m");
- String 타입인 username과 int 타입인 age를 함께 조회할 때에는 반환 타입이 명확하지 않다.
✅ 결과 조회 API
▶ getResultList()
결과가 하나 이상일 경우 사용하며, 리스트로 반환한다.
🔽 예시 코드
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
List<Member> resultList = query.getResultList(); //getResultList()
🔽 특징
- 결과가 없을 경우 빈 리스트를 반환한다.
▶ getSingleResult()
결과가 정확히 하나일 경우 사용하며, 단일 객체를 반환한다.
🔽 예시 코드
TypedQuery<Member> query = em.createQuery("select m from Member m where m.id = 1L", Member.class);
Member result = query.getSingleResult(); //getSingleResult()
🔽 특징
- 결과가 없을 경우 NoResultException 예외가 발생한다.
- 결과가 없을 경우 예외가 발생하는 것은 일반적이지 않는데, Spring Data JPA를 사용할 경우 null이나 Optional을 반환하는 방식으로 처리한다.
- 결과가 두 개 이상일 경우 NonUniqueResultException 예외가 발생한다.
✅ 파라미터 바인딩
▶ 이름 기준
이름 기준 파라미터 바인딩은 =: 을 사용해서 설정한다.
select m from Member m where m username =:username // =: 이름 기준 파라미터 바인딩
🔽 예시 코드
TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = :username", Member.class);
query.setParameter("username", "member1"); //이름 기준 파라미터 바인딩
Member result = query.getSingleResult();
🔽 메서드 체인 사용 예시 코드
Member result = em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", "member1") //이름 기준 파라미터 바인딩
.getSingleResult();
▶ 위치 기준
위치 기준 파라미터 바인딩은 =? 을 사용해서 설정한다.
select m from Member m where m username = ?1 // =? 위치 기준 파라미터 바인딩
🔽 예시 코드
TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = ?1", Member.class);
query.setParameter(1, "member1"); //위치 기준 파라미터 바인딩
Member result = query.getSingleResult();
🔽 메서드 체인 사용 예시 코드
Member result = em.createQuery("select m from Member m where m.username = ?1", Member.class)
.setParameter(1, "member1") //위치 기준 파라미터 바인딩
.getSingleResult();
⚠️ 위치 기준 파라미터 바인딩은 사용하지 않는 것이 좋다. 데이터베이스에는 값이 계속해서 추가되기 때문에, 위치가 변경될 가능성이 높기 때문이다.
✅ 프로젝션
프로젝션: JPQL의 SELECT 절에 조회할 대상을 지정하는 것
- SELECT 절의 조회 대상의 타입에 따라 프로젝션 종류가 달라진다.
- 엔티티 프로젝션
- 임베디드 타입 프로젝션
- 스칼라 타입 프로젝션
▶ 엔티티 프로젝션
엔티티 프로젝션은 SELECT 절의 조회 대상이 엔티티 객체인 경우를 말한다.
select m from Member m
select m.team from Member m
🔽 예시 코드
List<Member> resultList = em.createQuery("select m from Member m", Member.class).getResultList();
🔽 특징
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);
em.flush();
em.clear();
List<Member> resultList = em.createQuery("select m from Member m", Member.class).getResultList();
Member findMember = resultList.get(0);
findMember.setAge(20);
- 엔티티 프로젝션은 영속성 컨텍스트에서 관리된다.
- 위와 같이 영속성 컨텍스트가 완전히 비워진 상태에서 엔티티 프로젝션의 조회를 할 경우, 쿼리 결과를 통해 엔티티 속성을 변경할 수 있다.
▶ 임베디드 타입 프로젝션
임베디드 타입 프로젝션은 SELECT 절의 조회 대상이 엔티티의 값 타입 속성인 경우를 말한다.
select m.address from Member m
🔽 예시 코드
List<Address> resultList = em.createQuery("select m.address from Member m", Address.class).getResultList();
🔽 특징
//JPQL에서 불가능한 쿼리 (
select a from Address a
- 임베디드 값 타입은 특정 엔티티에 속해 있기 때문에 조회의 시작점이 될 수 없다.
- 위와 같은 쿼리는 불가능하다.
▶ 스칼라 타입 프로젝션
스칼라 타입 프로젝션은 SELECT 절의 조회 대상이 데이터 타입과 상관없이, 여러 데이터를 조회하는 경우를 말한다.
select m.username, m.age from Member m
🔽 예시 코드
List resultList = em.createQuery("select m.username, m.age from Member m").getResultList();
- 설명: username은 String 타입, age는 int 타입인데, 이 경우 결과 데이터를 조회하기 위한 여러 방법들이 존재한다.
🔽 여러 데이터를 조회하는 방법
1. Query 타입으로 조회
Query query = em.createQuery("select m.username, m.age from Member m");
2. 타입 캐스팅을 이용한 Object[] 타입으로 조회
List resultList = em.createQuery("select m.username, m.age from Member m").getResultList();
Object o = resultList.get(0);
Object[] result = (Object[]) o; //타입 캐스팅
System.out.println("result[0] = " + result[0]);
System.out.println("result[0] = " + result[1]);
- 설명: 위 조회 결과 List는 Object 타입으로 저장되는데, 따라서 각각의 username과 age를 조회하기 위해서는 배열로 캐스팅해야 한다.
List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m").getResultList();
Object[] result = resultList.get(0);
System.out.println("result[0] = " + result[0]);
System.out.println("result[0] = " + result[1]);
- 설명: 위와 같이 제너릭을 활용하여 타입 캐스팅도 가능하다.
3. new 명령어를 통한 DTO 조회
@Getter
@Setter
@AllArgsConstructor //생성자 필수
public class MemberDTO {
private String username;
private int age;
}
- 조회 용도의 DTO를 생성한다.
List<MemberDTO> resultList = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
.getResultList();
MemberDTO memberDTO = resultList.get(0);
System.out.println("memberDTO = " + memberDTO.getUsername());
System.out.println("memberDTO = " + memberDTO.getAge());
- SELECT 절에는 new 명령어와 함께 패키지 명을 포함한 전체 클래스 명을 작성한다.
- 순서와 타입이 일치하는 생성자가 필수이다.
- 패키지명을 작성해야 하는 불편함은 QueryDSL을 사용하여 극복할 수 있다.
3번 방법을 사용하는 것이 권장된다.
✅ 페이징 API
JPA는 페이징을 다음 두 가지 API로 추상화하여 제공한다.
- setFirstResult(int startPosition): 조회 시작 위치 (0부터 시작)
- setMaxResults(int maxResult): 조회할 데이터 수
🔽 예시 코드
List<Member> resultList = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(0) //조회 시작
.setMaxResults(10) //조회할 데이터 수
.getResultList();
- 설명: Member의 나이를 기준으로 내림차순(desc)으로 0부터 10개의 데이터를 조회한다.
✅ 조인
JPQL은 SQL과 마찬가지로 조인(JOIN)을 지원하는데, SQL 문법과는 약간 차이가 있다.
▶ 내부 조인(Inner Join)
내부 조인은 INNER JOIN을 사용한다.
select m from Member m [inner] join m.team t
- INNER는 생략이 가능하다.
🔽 예시 코드
String query = "select m from Member m inner join m.team t";
List<Member> resultList = em.createQuery(query, Member.class).getResultList();
- JPQL 조인은 연관 관계를 사용한다.
- Member m join m.team t: Member가 가지고 있는 연관 관계 필드로 Team과 조인
▶ 외부 조인(Outer Join)
외부 조인은 LEFT OUTER JOIN을 사용한다.
select m from Member m left [outer] join m.team t
- OUTER은 생략이 가능하다.
🔽 예시 코드
String query = "select m from Member m left outer join m.team t";
List<Member> resultList = em.createQuery(query, Member.class).getResultList();
▶ 세타 조인(Theta Join)
JPQL에서는 기본적으로 연관관계가 있는 엔티티들 간의 조인이 이루어진다. 그러나 세타 조인은 연관관계가 전혀 없는 엔티티들 간의 조인이 가능하기 때문에, 서로 연관관계가 없을 때 사용하는 방법 중 하나이다.
🔽 특징
- 세타 조인은 WHERE 절을 사용하여 작성할 수 있다.
- 세타 조인은 내부 조인만 지원한다.
- 전혀 관계가 없는 엔티티들 간의 조인이 가능하다.
🔽 예시 코드
String query = "select count(m) from Member m, Team t where m.username = t.name";
List<Member> resultList = em.createQuery(query, Member.class).getResultList();
▶ 조인 - ON절
JPA 2.1부터 조인할 때 ON 절을 지원하며, ON 절은 조인 대상을 필터링한 후 조인할 수 있게 해준다.
- 내부 조인에서 ON 절은 WHERE 절을 사용할 때와 결과가 같다.
- 보통 ON 절은 외부 조인에서만 사용한다.
- Hibernate 5.1부터 연관관계가 없는 엔티티들 간의 외부 조인이 가능하다.
🔽 조인 대상 필터링
- JPQL
select m, t from Member m left join m.team t on t.name = 'A'
- SQL
SELECT m.*, t.*
FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'
🔽 연관관계없는 엔티티 외부 조인
- JPQL
select m, t from Member m left join Team t on m.username = t.name
- SQL
SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name
✅ 서브 쿼리
JPQL도 SQL처럼 서브 쿼리를 지원한다.
▶ 서브 쿼리 지원 함수
🔽 [NOT] EXISTS
- EXISTS: 서브 쿼리에 결과가 존재하면 참이다.
- NOT EXISTS: 서브 쿼리에 결과가 없을 때 참이다.
//팀A 소속인 회원
select m from Member m where exists (select t from m.team t where t.name = 'teamA')
🔽 ALL | ANY | SOME
- ALL: 서브 쿼리의 결과가 모두 만족하면 참이다.
- ANY, SOME: 서브 쿼리의 결과 중 하나라도 만족하면 참이다.
- 비교 연산자(=, <>, <, <=, >, >=)와 함께 사용된다.
//전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o where o.orderAmount > ALL (select p.stockAmount from Product p)
//어떤 팀이든 팀에 소속된 회원
select m from Member m where m.team = ANY (select t from Team t)
🔽 [NOT] IN
- IN: 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참이다.
- NOT IN: 서브 쿼리의 결과 중 어느 것도 같은 것이 없을 때 참이다.
- IN은 서브 쿼리가 아닌 곳에서도 사용한다.
//20세 이상을 보유한 팀
select t from Team t where t IN (select t2 From Team t2 JOIN t2.members m2 where m2.age >= 20)
▶ 서브 쿼리 한계
- JPA 표준 스펙: 서브 쿼리는 WHERE 및 HAVING 절에서만 사용할 수 있다.
- Hibernate 구현체:
- Hibernate: SELECT 절에서도 서브 쿼리를 사용할 수 있다.
- Hibernate 6: FROM 절에서도 서브 쿼리를 사용할 수 있다.
- 이전 버전에서는 FROM 절에서 서브 쿼리를 사용할 수 없으며, 이 경우 조인으로 풀어서 해결해야 한다. (가능할 경우)
✅ 타입 표현
▶ 문자
- 작은따옴표(')를 사용하여 표현한다.
- 예: 'HELLO'
- 작은따옴표(')는 두 번 연속 사용하여 표현한다.
- 예: 'She''s'
🔽 예시 코드
- username이 'HELLO'인 회원
select m from Member m where m.username = 'HELLO'
▶ 숫자
- Long 타입: L 접미사를 사용한다.
- 예: 10L
- Double 타입: D 접미사를 사용한다.
- 예: 10D
- Float 타입: F 접미사를 사용한다.
- 예: 10F
- 정수: 접미사 없이 사용한다.
- 예: 10
🔽 예시 코드
- 가격이 10L인 상품
select p from Product p where p.price = 10L
▶ Boolean
- TRUE, FALSE
🔽 예시 코드
- available이 true인 상품
select p from Product p where p.available = true
▶ ENUM
- 패키지 이름을 포함한 전체 이름을 사용해야 한다.
- 예: jpabook.MemberType.Admin
🔽 예시 코드
- memberType이 Admin인 회원
select m from Member m where m.memberType = jpabook.MemberType.Admin
▶ 엔티티 타입
- 엔티티 타입을 표현하며 주로 상속 관계에서 사용된다.
- 예: TYPE(m) = Member (상속 관계에서 사용)
🔽 예시 코드
- Item 엔티티와 상속 관계에 있는 Book 엔티티
select i from Item i where type(i) = Book
✅ 기타식
JPQL은 SQL과 문법이 같은 식을 지원한다.
▶ 서브 쿼리
- [NOT] EXISTS: 서브 쿼리에 결과가 존재하면 참이다. NOT EXISTS는 결과가 없을 때 참이다.
- [NOT] IN: 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참이다. NOT IN은 어느 것도 같은 것이 없을 때 참이다.
🔽 예시 코드
- 주소 중 city가 New York인 주소가 하나라도 있는 사람
select p from Person p where exists (select a from p.address a where a.city = 'New York')
- 나이가 25, 30, 35인 사람
select p from Person p where p.age in (25, 30, 35)
위의 서브 쿼리 부분을 참고하자.
▶ 논리 연산
- AND: 두 조건을 만족하면 참이다.
- OR: 두 조건 중 하나만 만족해도 참이다.
- NOT: 조건식의 반대를 만족하면 참이다.
🔽 예시 코드
- 나이가 25보다 많고, city가 New York인 사람
select p from Person p where p.age > 25 and p.city = 'New York'
- city가 New York이 아닌 사람
select p from Person p where not p.city = 'New York'
▶ 비교 연산
- 비교 연산자: =, >, >=, <, <=, <>
🔽 예시 코드
- 나이가 30보다 큰 사람
select p from Person p where p.age > 30
▶ BETWEEN
- BETWEEN: 특정 범위 내에 값이 있는지 확인한다. 범위의 양 끝 값을 포함한다.
🔽 예시 코드
- 나이가 25와 35 사이(포함)에 있는 사람
select p from Person p where p.age between 25 and 35
▶ LIKE
- LIKE: 문자열 패턴과 일치하는 값을 조회한다.
🔽 예시 코드
- 이름이 J로 시작하는 모든 사람
select p from Person p where p.name like 'J%'
▶ IS [NOT] NULL
- IS NULL: 값이 NULL 인지 확인한다.
- IS NOT NULL: 값이 NULL이 아닌지 확인한다.
🔽 예시 코드
- 전화번호가 null이 아닌 사람
select p from Person p where p.phoneNumber is not null
✅ 조건식 - CASE 식
JPQL은 SQL과 마찬가지로 특정 조건에 따라 분기할 때 CASE 식을 사용한다.
- CASE 식의 4가지 종류
- 기본 CASE
- 단순 CASE
- COALESCE
- NULLIF
▶ 기본 CASE
여러 개의 조건식을 사용하여 각 조건이 참일 때 어떤 결과를 반환할지 정의한다.
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
▶ 단순 CASE
하나의 표현식을 기준으로 여러 조건을 비교하여 결과를 반환한다.
select
case t.name
when 'TeamA' then '인센티브110%'
when 'TeamB' then '인센티브120%'
else '인센티브105%'
end
from Team t
▶ COALESCE
스칼라식을 차례대로 하나씩 조회해서 null이 아니면 반환한다.
select coalesce(m.username, '이름 없는 회원') from Member m
- 설명: m.username이 null이면 '이름 없는 회원'을 반환한다.
▶NULLIF
두 값이 같으면 null을, 다르면 첫 번째 값을 반환한다.
select nullif(m.username, '관리자') from Member m
- 설명: m.username이 '관리자'이면 null을 반환하고, null이 아니면 m.username을 반환한다.
✅ 기본 함수
▶ CONCAT
- CONCAT(문자1, 문자2, ...): 문자를 합친다.
🔽 예시 코드
select concat('a', 'b') from Member m
- 설명: 'a'와 'b'를 연결하여 'ab'를 반환한다.
▶ SUBSTRING
- SUBSTRING(문자, 위치, [길이]): 위치부터 시작해 길이만큼 문자를 구한다.
- 위치: 추출을 시작할 위치
- 길이: 추출할 문자 수
🔽 예시 코드
select substring(m.username, 1, 3) from Member m
- 설명: username이 'soeun'인 경우 'soe'를 반환한다.
▶TRIM
- TRIM([ [ LEADING | TRAILING | BOTH ] [트림 문자] FROM ] 문자): 트림 문자를 제거한다.
- LEADING: 문자열의 왼쪽 제거
- TRAILING: 문자열의 오른쪽 제거
- BOTH: 문자열의 양쪽 제거 (기본값)
- 트림 문자: 제거할 문자를 지정 (기본값은 공백 문자(스페이스))
🔽 예시 코드
select trim(both ' ' from m.username) from Member m
- 설명: username이 ' soeun '인 경우 'soeun'을 반환한다.
▶ LOWER
- LOWER(문자): 소문자로 변경한다.
🔽 예시 코드
select lower(m.username) from Member m
- 설명: username이 'SOEUN'인 경우 'soeun'을 반환한다.
▶UPPER
- UPPER(문자): 대문자로 변경한다.
🔽 예시 코드
select upper(m.username) from Member m
- 설명: username이 'soeun'인 경우 'SOEUN'을 반환한다.
▶LENGTH
- LENGTH(문자): 문자 길이를 구한다.
🔽 예시 코드
select length(m.username) from Member m
- 설명: username이 'soeun'인 경우, 5를 반환한다.
▶ LOCATE
- LOCATE(찾을 문자, 원본 문자, [검색 시작 위치]): 검색 위치부터 문자를 검색한다.
- 찾을 문자: 검색할 문자열
- 원본 문자: 검색 대상 문자열
- 검색 시작 위치: 검색을 시작할 위치 (기본값은 1)
🔽 예시 코드
select locate('Soeun', m.username) from Member m
- 설명: username이 'LeeSoeun'인 경우 4를 반환한다.
▶ ABS
- ABS(수식): 절댓값으로 반환한다.
🔽 예시 코드
select abs(m.salary - 5000) from Member m
- 설명: salary에서 4000이면 -1000의 절댓값인 1000을 반환한다.
▶ SQRT
- SQRT(수식): 제곱근으로 변환한다.
🔽 예시 코드
select sqrt(m.salary) from Member m
- 설명: salary가 1600이면 40을 반환한다.
▶MOD
- MOD(수식, 나눌 수): 나머지를 구한다.
🔽 예시 코드
select mod(m.salary, 3) from Member m
- 설명: salary가 10이면 1을 반환한다.
▶ SIZE
- SIZE(컬렉션): 컬렉션의 크기(원소 개수)를 구한다.
🔽 예시 코드
select size(m.orders) from Member m
▶INDEX
- INDEX(별칭): LIST 타입 컬렉션의 위치 값
- 컬렉션이 @OrderColumn을 사용하는 LIST 타입일 때만 사용이 가능하다.
🔽 예시 코드
select index(t.members) from Team t
김영한 선생님께서는 쓰는 걸 추천하지 않는다고 하신다...
▶사용자 정의 함수
사용자 정의 함수를 Hibernate에서 사용하려면, 데이터베이스 방언을 상속받아 함수를 등록해야 한다.
방언(Dialect): Hibernate가 특정 데이터베이스와 상호 작용하는 방식을 정의하는 클래스
🔽 예시
예제: 'group_concat' 함수 등록
1. 방언 클래스 작성
public class MyH2Dialect extends H2Dialect {
public MyH2Dialect() {
// 'group_concat' 함수 등록
registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
}
}
2. Hibernate 설정 파일에서 방언 클래스 설정
<property name="hibernate.dialect" value="dialect.MyH2Dialect"/>
3. 사용자 정의 함수 호출
//JPQL에서 함수 호출
select function('group_concat', m.username) from Member m
//Hibernate 사용 시
select group_concat(m.username) from Member m
📍 참고
'Programming > JPA' 카테고리의 다른 글
[JPA] JPQL 중급 문법 - 객체지향 쿼리 언어 JPQL (2) (0) | 2024.08.09 |
---|---|
[JPA] 값 타입 컬렉션 - 값 타입 (4) (3) | 2024.07.25 |
[JPA] 값 타입 비교 - 값 타입 (3) (1) | 2024.07.25 |
[JPA] 값 타입과 불변 객체 - 값 타입 (2) (0) | 2024.07.25 |
[JPA] 기본값 타입과 임베디드 타입 - 값 타입 (1) (1) | 2024.07.25 |