1. JPQL의 소개
JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
JPQL은 결국 SQL 변환된다.
2. 벌크 연산
벌크 연산은 여러 개의 데이터를 조회하거나 수정, 삭제할 때 사용하는 것이다.

JPQL에 대한 학습 내용을 작성하기 전에 객체 모델과 DB 모델 그리고 실제 코드로 엔티티를 구현한 것을 먼저 적어보았다.

@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
@Entity
@Table(name = "ORDERS")
public class Order {
@Id
@GeneratedValue
private Long id;
private int orderAmount;
@Embedded
private Address address;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
private int stockAmount;
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
3. JPQL 문법
- 엔티티와 속성은 대소문자 구분을 한다.
- JPQL 키워드는 대소문자 구분을 하지 않는다.
- 엔티티의 이름을 사용한다. 테이블의 이름이 아니다.(ex. @Entity에 설정된 이름)
- 별칭은 필수이며, as는 생략이 가능하다.
- COUNT, SUM, AVG, MAX, MIN, GROUP BY, HAVING, ORDER BY와 같은 집합과 정렬도 사용 가능하다.

TypeQuery와 Query
TypeQuery는 반환 타입이 명확할 때 사용하고, Query는 반환 타입이 명확하지 않을 때 사용한다.

결과 조회 API
getResultList() : 결과가 하나 이상일 때 사용하며, 리스트를 반환한다. 결과가 없으면 빈 리스트를 반환한다.
getSingleResult() : 결과가 정확히 하나일 때 사용하며, 단일 객체를 반환한다.
결과가 없으면 NoResultException이 발생하고, 결과가 둘 이상이면 NonUniqueResultException이 발생한다.

파라미터 바인딩 - 이름 기준, 위치 기준
아래와 같이 바인딩을 통해 원하는 조건에 해당하는 데이터도 가져올 수 있다.
1) 이름 기준 파라미터 바인딩

2) 위치 기준 파라미터 바인딩

위치 기반의 파라미터 바인딩은 조건에 중간에 추가되면 숫자를 바꾸고 수정이 많이 일어나니 위치 기준보다는 이름 기준 파라미터 바인딩을 사용하자
4. 프로젝션
SELECT 절에 조회할 대상을 지정하는 것을 말한다.
프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 있다.
DISTINCT로 중복을 제거할 수 있다.
SELECT m FROM Member m -> 엔티티 프로젝션
SELECT m.team FROM Member m -> 엔티티 프로젝션
SELECT m.address FROM Member m -> 임베디드 타입 프로젝션
SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
엔티티 프로젝션은 영속성 컨텍스트에서 관리가 되어 set을 통해서 데이터를 변경하면 UPDATE 쿼리를 날린다.
프로젝션 - 여러 값 조회
1) Query 타입으로 조회

2) Object[] 타입으로 조회


3) new 명령어로 조회
단순 값을 DTO로 바로 조회하는 방법이다.
패키지 명을 포함한 전체 클래스 명 입력해줘야한다.
순서와 타입이 일치하는 생성자 필요하다.
DTO로 담기 위해서 MemberDTO를 하나 만들어 주고,
package hellojpa;
public class MemberDTO {
private String username;
private int age;
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
아래와 같이 코드를 구현하면 된다.

5. 페이징 API
JPA는 페이징을 다음과 같이 두 API로 추상화하였다.
- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
- setMaxResult(Int maxResult) : 조회할 데이터 수
사용하는 예시는 아래와 같다.
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
for (int i = 0; i < 100; i++) {
Member member = new Member();
member.setUsername("member" + i);
member.setAge(i);
em.persist(member);
}
em.flush();
em.clear();
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(1)
.setMaxResults(10)
.getResultList();
System.out.println("result.size = " + result.size());
for(Member member1 : result) {
System.out.println("member1 = " + member1);
}
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
위의 코드를 실행시키면 아래와 같이 쿼리문이 나가고 페이징 처리가 되어서 데이터가 나오는 것을 확인할 수 있다.

6. 조인(JOIN)
1) 내부 조인
SELECT m FROM Member m [INNER] JOIN m.tem t
2) 외부 조인
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
3) 세타 조인: 연관관계가 없어도 JOIN을 해서 가져오는 것
SELECT COUNT(m) from Member m, Team t where m.username = t.name
코드 예시

코드를 실행해서 쿼리문을 확인해 보면 아래와 같은 쿼리가 나가는 것을 확인할 수 있다.
=============================
Hibernate:
/* select
m
from
Member m
inner join
m.team t */ select
m1_0.id,
m1_0.age,
m1_0.TEAM_ID,
m1_0.username
from
MEMBERS m1_0
join
Team t1_0
on t1_0.id=m1_0.TEAM_ID
=============================
Hibernate:
/* select
m
from
Member m
left outer join
m.team t */ select
m1_0.id,
m1_0.age,
m1_0.TEAM_ID,
m1_0.username
from
MEMBERS m1_0
=============================
Hibernate:
/* select
m
from
Member m,
Team t
where
m.username = t.name */ select
m1_0.id,
m1_0.age,
m1_0.TEAM_ID,
m1_0.username
from
MEMBERS m1_0,
Team t1_0
where
m1_0.username=t1_0.name
조인의 ON절(JPA 2.1부터 지원)
1) 조인 대상 필터링
ex. 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
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'
2) 연관관계 없는 엔티티 외부 조인
연관관계 없는 엔티티 외부 조인할 수 있다.(하이버네이트 5.1부터 사용 가능하며, 과거에는 내부 조인만 가능했음)
ex) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
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
7. 서브 쿼리(SUBQUERY)
JPQL에서는 서브 쿼리도 지원한다.
서브 쿼리 지원 함수
- [NOT] EXISTS (subquery) : 서브 쿼리에 결과가 존재하면 참
- ALL | ANY | SOME (subquery)
- ALL은 모두 만족하면 참
- ANY, SOME은 같은 의미이며, 조건을 하나라도 만족하면 참
- [NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
서브 쿼리 예시를 살펴보자
나이가 평균 보다 많은 회원
SELECT m
FROM Member m
WHERE m.age > (SELECT AVG(m2.age) FROM Member m2)
한 건이라도 주문한 고객(위에 있는 대상을 서브쿼리로 가져오는 경우, 성능이 잘 안나온다)
SELECT M
FROM Member m
WHERE (SELECT COUNT(o) FROM Order o WHERE m = o.member) > 0
팀A 소속인 회원
SELECT m
FROM Member m
WHERE EXISTS (SELECT t FROM m.team t WHERE t.name = '팀A')
전체 상품 각각의 재고보다 주문량이 많은 주문건들
SELECT o
FROM Order o
WHERE o.orderAmount > ALL(SELECT p.strockAmount FROM Product p)
어떤 팀이든 팀에 소속된 회원
SELECT m
FROM Member m
WHERE m.team = ANY(SELECT t FROM Team t)
JPA 서브 쿼리 한계
- JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용이 가능하다.(JPA 표준 스펙에서만)
- SELECT 절도 가능하다.(하이버네이트에서 지원)
- FROM 절의 서브 쿼리는 현재 JPQL에서 불가능하다. 조인으로 풀 수 있으면 풀어서 해결
- 하이버네이트6 부터는 FROM 절의 서브쿼리를 지원한다.
8. JPQL 타입 표현
- 문자 : 'HELLO', 'She''s'
- 숫자 : 10L(Long), 10D(Double), 10F(float)
- Boolean : TRUE, FALSE
- Enum : jpa.MemberType.Admin (쿼리 안에 직접 사용하면 패키지명을 포함시켜야 한다. setParameter로 파라미터 바인딩을 하면 쿼리문 자체가 길어지지 않는다)
- 엔티티 타입 : TYPE(m) = Member (상속 관계에서 사용)
- SQL과 문법이 같은 식을 사용한다.
- EXISTS, IN, AND, OR, NOT, =, >, >=, <, <= <>, BETWEEN, LIKE, IS NULL
// 엔티티 타입
em.createQuery("SELECT i FROM Item i WHERE type(i) = Book", Item.Class)
.getResultList();
9. 조건식 - CASE식
JPQL에서는 CASE도 사용이 가능하다.
기본 CASE식
SELECT
CASE WHEN m.age <= 10 THEN '학생요금'
WHEN m.age >= 60 THEN '경로요금'
ELSE '일반요금'
END
FROM Member m
단순 CASE식
SELECT
CASE t.name
WHEN '팀A' THEN '인센티브110%'
WHEN '팀B' THEN '인센티브120%'
ELSE '인센티브105%'
END
FROM Team t
CASE식을 사용하는 간단한 예제는 아래와 같다.
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member");
member.setAge(20);
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
String query =
"select" +
" case when m.age <= 10 then '학생요금'" +
" when m.age >= 60 then '경로요금'" +
" else '일반요금'" +
" end" +
" from Member m";
List<String> result = em.createQuery(query, String.class).getResultList();
for (String s : result) {
System.out.println("s = " + s);
}
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}

COALESCE : 하나씩 조회해서 null이 아니면 반환
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername(null);
member.setAge(20);
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
String query = "select coalesce(m.username, '이름 없는 회원') from Member m";
List<String> result = em.createQuery(query, String.class).getResultList();
for (String s : result) {
System.out.println("s = " + s);
}
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}

NULLIF: 두 값이 같으면 null을 반환하고, 다르면 첫번째 값을 반환
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("관리자");
member.setAge(20);
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
String query = "select nullif(m.username, '관리자') from Member m";
List<String> result = em.createQuery(query, String.class).getResultList();
for (String s : result) {
System.out.println("s = " + s);
}
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}

COALESCE와 NULLIF는 표준함수이기 때문에 어떤 DB를 사용하더라도 제공을 해준다.
10. JPQL 기본 함수
아래와 같이 JPQL은 기본 함수를 제공한다.
- CONCAT
- SUBSTRING
- TRIM(LTRIM, RTRIM)
- LOWER, UPPER
- LENGTH
- LOCATE
- ABS,SQRT,MOD
- SIZE: 컬렉션의 크기를 돌려준다("select size(t.members) from Team t";)
- INDEX: @OrderColumn 리스트의 값 타입일 때 옵션을 줘서 사용할 수 있는데, 컬렉션의 위치 값을 구할 때 사용할 수 있음(JPA 용도)
JPQL에서 제공하는 표준함수는 DB와 상관없이 사용할 수 있다. 사용이 안된다면 하이버네이트는 사용전 방언에 추가해야 한다.
사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다. 아래와 같이 표준 문법을 제공한다
select function('group_concat', i.name) from Item i
각 DB에서 제공하는 registerFunction이 있어서 DB 종속적인 함수를 사용할 수 있다.
일반 벤더들이 제공하지 않는다면 사용자 정의 함수를 등록하면 된다!
아래와 같이 Dialect를 확장해서 구현해 주면 된다.
import org.hibernate.dialect.H2Dialect;
import org.hibernate.dialect.finction.StandardSQLFunction;
import org.hibernate.type.StandardBasicTypes;
public class MyH2Dialect extends H2Dialect {
public MyH2Dialect() {
registerFunction("group_concat", new StandardSQLFunction("group_concat", new StandardBasicTypes.String);
}
}
그리고 persistence에 가서 value를 패키지명.Dialect클래스명으로 변경해주면 사용자 정의 함수를 사용할 수 있다.
참고) Hibernate 버전 6.x인 경우 사용자 정의 함수
스프링 부트 3.x 버전을 사용하고 hibernate 6 부터는 diarect로 사용자 정의 함수를 만들 수 없다.
FunctionContributer의 구현체를 만들어 주어야 한다.
custom이라는 패키지를 만들고 그 안에 아래와 같이 구현체를 만들어 주고
package custom;
import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.boot.model.FunctionContributor;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.type.StandardBasicTypes;
public class CustomFunction implements FunctionContributor {
@Override
public void contributeFunctions(FunctionContributions functionContributions) {
functionContributions.getFunctionRegistry()
.register("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
}
}
src/main/resources/META-INF 안에
services 디렉토리를 만들어주고 org.hibernate.boot.model.FunctionContributor 파일을 생성해준 다음에

패키지명.컨트리뷰터이름 형태로 등록을 해주면 사용자 정의 함수를 사용할 수 있다.

String query = "select function('group_concat', m.username) from Member m";
주의) persistence의 diarect는 변경하지 않아도 된다.
'TIL > JPA' 카테고리의 다른 글
[TIL/JPA] 기본개념 : 페치 조인(fetch join) (0) | 2024.09.11 |
---|---|
[TIL/JPA] 기본개념 : 경로 표현식 (0) | 2024.09.10 |
[TIL/JPA] 기본개념 : JPA의 다양한 쿼리 방법 소개 (0) | 2024.09.04 |
[TIL/JPA] 기본개념 : 값 타입 (0) | 2024.08.31 |
[TIL/JPA] 기본개념 : 영속성전이(CASCADE), 고아 객체 (0) | 2024.08.27 |