![[자바 ORM 표준 JPA 프로그래밍 - 기본편] 11. 객체지향 쿼리 언어 2](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEr8Dl%2FbtsC4EMkHjA%2F8WQMXnt3Fumhg9gMvKgfX1%2Fimg.png)
인프런 김영한 강사님의 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 을 수강하고 정리한 글입니다.
자바 ORM 표준 JPA 프로그래밍 - 기본편 강의 - 인프런
현업에서 실제로 JPA로 개발을 하고 있습니다. 그런 입장에서보면 지금 작성하고 있는 코드들이 어떻게 작동하는지 이해하는데 큰 도움을 주는 강의입니다. 다음은 제가 느낀 이 강의의 장점들
www.inflearn.com
📌 경로 표현식
✅ 경로 표현식
점 (.) 을 찍음으로서 객체 그래프를 탐색하는 것을 말한다.
상태 필드 (static field) : 단순히 값을 저장하기 위한 필드 (ex> username)
상태 필드는 경로 탐색의 끝이며, 탐색할 수 없다.
연관 필드 (association field) : 연관관계를 위한 필드
- 단일 값 연관 필드 : @ManyToOne, @OneToOne과 같이 대상이 엔티티인 경우
- 묵시적 내부 조인 (Inner Join)이 발생하며, 탐색할 수 있다.
- 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany와 같이 대상이 컬렉션인 경우
- 마찬가지로 묵시적 내부 조인이 발생하지만, 탐색 할 수 없다.
- FROM 절에서 명시적 조인을 통해 별칭을 얻게 되면 별칭을 통해 탐색 가능하다.
명시적 조인 : join 키워드를 명시적으로 직접 사용하는 것을 말한다.
묵시적 조인 : 경로 표현식에 의해 묵시적으로 SQL 상 조인이 발생하는 것을 말한다. (내부 조인만 가능하다.)
실무에서는 가급적 묵시적 조인보다 명시적 조인을 사용한다.
묵시적 조인은 조인의 발생 상황을 한눈에 파악하기 어렵기 때문이다.
📌 Fetch Join
✅ 페치 조인 (Fetch Join)
이것은 SQL에 존재하는 조인의 종류가 아니다.
그저 JPQL에서만 성능 최적화를 위해 제공하는 기능이다.
연관된 엔티티나 컬렉션을 SQL 쿼리 한 번만으로 함께 조회하는 기능이다.
join fetch 라는 명령어를 활용한다.
✅ 엔티티 페치 조인
회원을 조회하면서 연관된 팀도 함께 조회한다. (쿼리 한 번으로)
// JPQL
select m from Member m join fetch m.team
// SQL
select M.*, T.* from MEMBER M inner join TEAM T on M.TEAM_ID = T.ID
✅ 컬렉션 페치 조인
일대다 관계에서 사용한다.
// 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'
✅ DISTINCT
DISTINCT는 SQL에서 중복 결과를 제거하는 명령이다.
JPQL에서의 DISTINCT는 2 가지 기능을 수행한다.
1. SQL에서 중복 결과를 제거하는 명령
2. 애플리케이션에서 엔티티 중복 제거
// SQL
select distinct t
from Team t join fetch t.members
where t.name = '팀A'
SQL에서는 DISTINCT를 추가하지만 데이터가 다르므로 결과에서 달라지는 것은 없다.
JPQL에서는 추가적으로 애플리케이션에서 중복 제거를 시도한다.
따라서 같은 식별자를 가진 Team 엔티티를 제거해버린다.
JPQL에서 DISTINCT를 추가한 결과는 아래와 같다.
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
Hibernate 6 버전 부터는 별도의 DISTINCT 명령어 없이도 애플리케이션에서 중복 제거가 자동으로 적용된다.
✅ 페치 조인과 일반 조인의 차이
기본적으로 JPQL은 결과를 반환할 때 연관관계는 고려하지 않는다.
일반 조인은 연관된 엔티티까지 함께 조회하지는 않는다.
페치 조인을 사용하게 되면 연관된 엔티티도 함께 조회해버린다. (즉시 로딩)
따라서 페치 조인은 객체 그래프를 SQL 쿼리 한 번으로 조회해버리는 개념인 것이다.
✅ 페치 조인의 한계
페치 조인 대상에는 별칭을 줄 수 없다.
데이터 정확성에 문제가 있을 수 있기 때문이다.
둘 이상의 컬렉션은 페치 조인 할 수 없다.
마찬가지로 데이터 정확성에 문제가 있을 수 있기 때문이다.
컬렉션을 페치 조인하게 되면 페이징 API를 사용할 수 없다.
일대일, 다대일 같은 단일 값 연관 필드들은 페이징이 가능하지만, 컬렉션의 경우는 데이터의 뻥튀기가 발생하여 페이징을 할 수 없다.
@BatchSize로 이 문제를 해결하긴 하는데, 다소 깊은 내용일 수 있다.
실무에서는 기본적으로 로딩 전략이 지연 로딩이다.
따라서 최적화가 필요한 곳만 페치 조인을 적용하는 것을 권한다.
📌 다형성 쿼리
✅ TYPE
조회 대상을 특정 자식으로 한정한다.
ex> Item 중에서 Book, Movie를 조회해라.
// JPQL
select i from Item i
where type(i) IN (Book, Movie)
// SQL
select i from i
where i.DTYPE in ('B', 'M')
📌 엔티티 직접 사용
✅ 엔티티 직접 사용 : 기본 키 값
JPQL에서 엔티티를 직접 사용하게 되면 SQL에서 해당 엔티티의 기본 키 값을 사용하게 된다.
// JPQL
select count(m.id) from Member m // 엔티티의 아이드를 사용
select count(m) from Member m // 엔티티를 직접 사용
// SQL
select count(m.id) as cnt from Member m
// 엔티티를 파라미터로 전달
String jpql = “select m from Member m where m = :member”;
List resultList = em.createQuery(jpql)
.setParameter("member", member)
.getResultList();
// 식별자를 파라미터로 전달
String jpql = “select m from Member m where m.id = :memberId”;
List resultList = em.createQuery(jpql)
.setParameter("memberId", memberId)
.getResultList();
// 실행된 SQL (둘 다 같은 SQL이 발생한다!!)
select m.* from Member m where m.id = ?
✅ 엔티티 직접 사용 : 외래 키 값
// 엔티티를 파라미터로 전달
Team team = em.find(Team.class, 1L);
String qlString = “select m from Member m where m.team = :team”;
List resultList = em.createQuery(qlString)
.setParameter("team", team)
.getResultList();
// 외래 키 식별자를 직접 전달
String qlString = “select m from Member m where m.team.id = :teamId”;
List resultList = em.createQuery(qlString)
.setParameter("teamId", teamId)
.getResultList();
// 실행되는 SQL (위 두가지 방식 모두 같은 SQL이 처리된다.)
select m.* from Member m where m.team_id=?
📌 Named 쿼리
✅ Named 쿼리 : 정적 쿼리
@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();
미리 정의해서 이름을 부여해놓고 사용하는 JPQL
정적쿼리이고, 어노테이션이다.
애플리케이션 로딩 시점에 초기화 후 쿼리를 검증하고, 이후에 재사용하는 방식이다.
혹시 쿼리에 오타가 잇을 때 Error 표시가 발생하기 때문에 에러를 발견하기 쉽다. (런타임 시점 에)
Named 쿼리는 XML에 정의할 수도 있다.
기본적으로 XML에 정의한 것이 항상 우선권을 가진다.
📌 벌크 연산
✅ 벌크 연산
한 번에 여러 개가 수정이 발생할 때, JPA의 변경 감지 기능만으로 모든 것을 수정하기에는 너무 많은 SQL 쿼리가 발생하는 경우가 있다.
예를 들어 변경된 데이터가 1000건이라면 1000번의 Update 쿼리가 발생하는 것이다.
이를 해결하기 위해 벌크 연산이 존재한다.
String qlString = "update Product p " +
"set p.price = p.price * 1.1 " +
"where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(qlString)
.setParameter("stockAmount", 10)
.executeUpdate();
쿼리 한 번으로 여러 테이블의 로우를 변경할 수 있다.
executeUpdate()의 결과는 영향받은 엔티티 수를 반환한다.
벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리하는 방식이다.
이 때문에 생기는 문제가 많을 수 있다.
이를 방지하기 위해서는, 벌크 연산을 가장 먼저 실행하는 것이다.
벌크 연산을 가장 먼저 수행하고, 그 이후 영속성 컨텍스트를 한번 초기화 해주면 문제가 생기지 않는다.
'Backend > JPA' 카테고리의 다른 글
[실전! 스프링 데이터 JPA] 2. 쿼리 메소드 기능 (1) | 2024.01.10 |
---|---|
[실전! 스프링 데이터 JPA] 1. 공통 인터페이스 기능 (0) | 2024.01.05 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 10. 객체 지향 쿼리 언어 1 (0) | 2024.01.05 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 9. 값 타입 (0) | 2024.01.05 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 8. 프록시와 연관관계 관리 (0) | 2024.01.05 |
개발자가 되고 싶어요.