JPA는 SQL을 추상화한 JPQL은 객체 지향 쿼리 언어를 제공
JPQL은 테이블이 아닌 엔티티 객체를 대상
특정 데이터베이스 SQL에 의존 X
즉, 객체 지향 SQL
참고
JPA를 사용하면서 JDBC 직접 사용 Template 등을 사용하게 되면
SQL을 실행하기 전에 영속성 컨텍스트를 수동 플러시 하자
✔️ 문법
select m from Member (as) m where m.age > 18
엔티티와 속성은 대소문자 구분 O (Member, age 등)
JPQL 키워드는 대소문자 구분 X (SELECT, select 등)
엔티티 이름 사용
💠 집합과 정렬
select
COUNT(m), //회원수
SUM(m.age), //나이 합
AVG(m.age), //평균 나이
MAX(m.age), //최대 나이
MIN(m.age) //최소 나이
from Member m
GROUP BY, HAVING, ORDER BY 사용가능
💠 TypeQuery, Query
TypeQuery : 반환 타입이 명확할 때 사용
Query : 반환 타입이 불명확할 때 사용
TypedQuery<Member> query =
em.createQuery("SELECT m FROM Member m", Member.class); // Member.class 를 적어둬서 타입을 명확하게 가리킴
Query query =
em.createQuery("SELECT m.username, m.age from Member m");
결과 조회 API
query.getResultList() // 결과가 하나 이상 , 리스트 반환 , 없으면 빈 리스트 반환
query.getSingleResult() // 결과가 정확히 하나, 단일 객체 반환
// 없으면 jakarta.persistence.NoResultException
// 둘이상이면 jakarta.persistence.NonUniqueResultException
파라미터 바인딩
"SELECT m FROM Member m where m.username=:username" // 컬럼=:파라미터명
query.setParameter("username", usernameParam);
프로젝션
SELECT 절에 조회 대상을 지정하는 것
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 // 스칼라 타입 프로젝션
✔️ 여러 값 조회
1. Query 타입으로 조회
아래와 유사함
2. Object[] 타입으로 조회
Member member = new Member();
member.setUsername("1번학생");
member.setAge(12);
Member member1 = new Member();
member1.setUsername("2번학생");
member1.setAge(22);
em.persist(member);
em.persist(member1);
em.flush();
em.clear();
List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m")
.getResultList();
for (Object[] objects : resultList) {
for (Object info : objects) {
System.out.print(info + " ");
}
System.out.println();
}
3. new 명령어로 조회
- 단순 값을 DTO로 바로 조회
- SELEECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
- 패키지명을 포함한 전체 클래스 명 입력
- 순서와 타입이 일치하는 생성자 필요
package jpql.jpql;
public class MemberDTO {
private String username;
private int age;
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
// getter , setter ,toString 메소드들..
}
List<MemberDTO> resultList = em.createQuery("select new jpql.jpql.MemberDTO(m.username, m.age) from Member m")
.getResultList();
for (MemberDTO memberDTO : resultList) {
System.out.println("memberDTO = " + memberDTO);
}
✔️ 페이징
setFristResult(int start Position) : 조회 시작 위치 (0부터 시작)
setMaxResults(int maxResult) : 조회할 데이터 수
List<Member> resultList = em.createQuery("select m from Member m", Member.class)
.setFirstResult(0) // 0부터
.setMaxResults(1) // 1명
.getResultList();
for (Member member2 : resultList) {
System.out.println("member2 = " + member2);
}
✔️ 조인
// 내부 조인
SELECT m FROM Member m [INNER] JOIN m.team t
// 외부 조인
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
// 세타 조인
SELECT COUNT(m) FROM Member m. Team t where m.username = t.name
💠 ON 절
// 1. 조인 대상 필터링
// 2. 연관관계 없는 엔티티 외부 조인
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
// SQL에서는
SQL:
SELECT m.*, t.* FROM
Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'
서브 쿼리도 가능
하이버네이트 6부터 FROM도 가능
✔️ 조건식
CASE식
COALESCE: 하나씩 조회해서 null이 아니면 반환
select coalesce(m.username, '이름이 없는 회원') from Member m
NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
select NULLIF(m.username, '관리자') from Member m
- CONCAT
- SUBSTRING
- TRIM
- LOWER, UPPER
- LENGTH
- LOCATE
- ABS
- SQRT
- MOD
- SIZE, INDEX(JPA 용도)
사용자 정의 함수도 가능
이는 하이버네이트가 버전 업데이트 후 바뀌어서 직접 찾아보고 바꿔야함
자세한 건 필요할때 찾아서 사용하자
✔️ 경로 표현식
.(점)을 찍어 객체 그래프를 탐색
select m.username -> 상태 필드
from Member m
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'
상태 필드 : 단순 값 저장 필드 , 경로 탐색의 끝, 탐색 x
연관 필드 : 연관관계를 위한 필드
- 단일 값 : 묵시적 내부 조인 발생, 탐색 o
- 컬렉션 값 : 묵시적 내부 조인 발생, 탐색 x
- FROM 절에서 명시적 조인을 통해 탐색 가능
// 단일 값 연관 필드 하나의 객체 엔티티
@ManyToOne, OneToOne, 대상이 엔티티 (m.team)
// 컬렉션 값 연관 필드 여러개 객체 컬렉션 엔티티
@OneToMany, @ManyToMany, 대상이 컬렉션 (m.orders)
// 단일 값 연관 경로 탐색
select o.members from Order o
// 묵시적 내부조인
SQL:
select m.*
from Orders o
inner join Member m on o.member_id = m.id
💠 명시적 조인
select m from Member m join m.team t // join 키워드를 통해 team을 명시
select t.members.username from Team t -> 실패
select m.username from Team t join t.members m -> 성공
컬렉션은 조회하면 타입이 컬렉션 타입으로 되기에 탐색이 불가능
따라서 명시적 조인이 필요
결론
직접 쿼리를 날릴 경우
무 조 건 명시적 조인 사용, 내부적 조인 사용 금지
✔️ 페치 조인
SQL 조인 종류 x
JPQL에서 성능 최적화를 위해 제공하는 기능
연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회 가능
select m from Member m join fetch m.team
참고
하이버네이트 6 부터 DISTINCT를 쓰지 않아도 자동으로 엔티티의 중복을 제거
일반 조인과 페치조인 차이점
일반 조인은 연관된 엔티티를 함께 조회 X
그저 select 에 지정한 엔티티만 조회
페치 조인은 연관된 엔티티도 함께 조회 (즉시 로딩)
페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념
[JPQL]
select t
from Team t join fetch t.members
where t.name = '팀A'
[SQL]
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
페치조인의 특징과 한계
페치 조인 대상에는 별칭을 줄 수 없다. (하이버네이트는 가능하지만 쓰지말자)
둘 이상의 컬렉션은 페치 조인 불가능
컬렉션을 페치 조인하면 페이징 API 사용 불가
- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징 (위험)
엔티티와 직접 적용하는 글로벌 로딩 전략보다 우선함
@OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
최적화가 필요한 곳에 사용
단, 여러 테이블을 조인한 다음 엔티티 그 자체의 값이 아닌 특정 값만 골라서 결과를 내야한다면
페치 조인보다는 일반 조인 후 필요한 데이터들만 조회해서 DTO로 반환하는게 더 효과적
🌱 컬렉션을 페치조인한 것처럼 하고 페이징 하기
컬렉션 페치 조인 대신
컬렉션에 @Batch_size()를 붙이고, Team을 조회하고 member에 각각 접근하면
where에 in 으로 조회한 Team과 연관된 모든 멤버를 다 가져온다.
이를 통해 N+1 문제가 해결된다.
@BatchSize(size = 100) // in 쿼리 ? 100개를 날리겠다는 의미
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
/**
* 회원 1과 회원 2는 TeamA
* member 3은 teamB 로 엔티티를 저장한 상황
*/
List<Team> result = em.createQuery("select t from Team t ", Team.class)
.setFirstResult(0)
.setMaxResults(100)
.getResultList();
💠다형성 쿼리
조회 대상을 특정 자식으로 한정
상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
FROM, WHERE, SELECT(하이버네이트 지원) 사용
[JPQL]
select i from Item i
where type(i) IN (Book, Movie)
[SQL]
select i from i
where i.DTYPE in (‘B’, ‘M’)
[JPQL]
select i from Item i
where treat(i as Book).author = ‘kim’
[SQL]
select i.* from Item i
where i.DTYPE = ‘B’ and i.author = ‘kim’
💠 엔티티 직접 사용
JPQL 에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용
[JPQL]
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용
[SQL](JPQL 둘다 같은 다음 SQL 실행)
select count(m.id) as cnt from Member m
✔️ Named 쿼리
미리 이름과 정적 쿼리를 정의해두고 사용 (XML , 애노테이션 정의)
애플리케이션 로딩 시점에 쿼리를 검증 및 초기화 후 재사용
@Entity
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username","회원1")
.getResultList();
스프링 데이터 JPA에서 인터페이스로 메서드 이름만 혹은 쿼리를 작성하여 사용(Named 쿼리)
✔️ 벌크 연산
변경된 데이터가 100개라 했을 때 100번의 update 쿼리를 날릴 순 없다.
벌크 연산을 통해 쿼리 한번으로 여러 테이블 로우를 변경(엔티티)
UPDATE, DELETE 지원
INSERT(insert into.. select, 하이버네이트 지원)
int resultCount = em.createQuery("update Product p set p.price = p.price * 2 where p.price < :price")
.setParameter("price", 1000)
.executeUpdate(); // 영향받은 엔티티 수 반환
주의 사항
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리
벌크 연산을 먼저 실행 후 영속성 컨텍스트 초기화
벌크 연산으로 엔티티의 특정 값들을 변경하고
find 해도 영속성 컨텍스트는 그대로이기 때문에 캐시에 있는 변경 전의 값을 그대로 반환한다.
따라서 em.clear() 한 뒤 find 해야 db에서 다시 가져온다.
🔖 학습내용 출처
자바 ORM 표준 JPA 프로그래밍 - 기본편 / 김영한
김영한. (2021). 자바 ORM 표준 JPA 프로그래밍. 에이콘출판사
'Back-End > JPA' 카테고리의 다른 글
스프링 데이터 JPA - 기본 (0) | 2024.07.29 |
---|---|
API 학습 내용 (0) | 2024.07.15 |
JPA - 값타입 (0) | 2024.07.12 |
JPA - 프록시와 연관관계 관리 (0) | 2024.07.10 |
JPA - 고급 매핑 (0) | 2024.07.09 |