객체지향 쿼리 언어2
1. JPQL- 경로 표현식
- .(점)을 찍어 객체 그래프를 탐색 하는 것
- m.username -> 상태 필드
- m.team -> 단일 값 연관 필드
- m.orders -> 컬렉션 값 연관 필드
1) 용어 정리
- 상태필드: 단순히 값을 저장하기 위한
- 경로 탐색의 끝
- 탐색 x
- 연관필드: 연관관계를 위한 필드
- 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티
- 묵시적 내부 조인(inner join) 발생, 탐색o
- 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션
- 묵지석 내부 조인 발생, 탐색 x
- From 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
- 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티
2) 명시적 조인, 묵시적 조인
- 명시적 조인: join 키워드 직접 사용
select m from Member m join m.team t
- 묵시적 조인: 경로 표현식에 의해 묵지석으로 SQL 조인 발생(내부조인만 가능)
select m.team from Member m
3) 예제
select o.member.team from Order o
-> 성공select t.members from Team
-> 성공select t.members.username from Team t
-> 실패select m.username from Team t join t.members m
-> 성공
4) 묵시적 조인 주의사항
- 항상 내부 조인만 가능
- 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야함
- 경로 탐색은 주로 select, where절에서 사용하지만 묵시적 조인으로 인해 SQL의 From(join)절에 영향을 줌
- 가급적 명시적 조인 사용
- 조인은 SQL 튜닝에 중요 포인트
- 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움
2. JPQL - 페치 조인(fetch join)
- SQL 조인 종류가 아님
- JPQL에서만 지원하는 성능 최적화 기능
- 연관된 엔티티나 컬렉션을 SQL로 한번에 함께 조회하는 기능
- (left) join fetch를 통해 사용
1) 엔티티 페치 조인(일대일, 다대일)
회원을 조회하면서 연관된 팀도 함께 조회(SQL 한번에)
회원과 팀을 한번에 조회하여 영속성 컨텍스트(1차캐시)에 저장
JPQL
select m from Member m join fecth m.team
SQL
SELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
2) 일반 조인과 다른점
// 일반 조인
String jpql = "select m from Member m join m.team t";
List<Member> members = em.createQuery(jpql, Member.class)
.getResultList();
for (Member member : members) {
System.out.println("username = " + member.getUsername() + ", " +
"teamName = " + member.getTeam().getName());
}
- JPQL은 결과를 반환할 때 연관관계를 고려하지 않음. 단지 select절에 지정한 엔티티만 조회할 뿐.
- 모든 연관관계의 fetch방식을 Lazy로 했기 때문에 member.team에는 프록시 객체가 들어가 있음
- 연관된 team엔티티를 함께 조회하지 않음
- member.getTeam().getName()을 호출 했을 때 프록시 객체가 초기화 되면서 DB에 쿼리가 날라감
- 만약 Member객체 모두 팀이 다 다르다면 1 + N 만큼 쿼리가 날라감.
- 즉 select m 을 했는데 1(select쿼리) + N(엔티티 Member의 갯수)만큼 쿼리가 추가적으로 날라감
- 성능에 심각한 문제
// 페치 조인
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
.getResultList();
for (Member member : members) {
System.out.println("username = " + member.getUsername() + ", " +
"teamName = " + member.getTeam().getName());
}
위의 엔티티 패치 조인에서 봤듯이 다음과 같은 SQL이 날라감
SELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
team 엔티티또한 member 엔티티를 가져올때 같이 가져옴(즉시로딩).
둘다 프록시가 아닌 실제 엔티티가 들어감
한번에 가져오기 때문에 쿼리가 한번만 날라감.
페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념
성능 최적화
3) 컬렉션 페치 조인(일대다)
- 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'
(1) 문제점
팀 A에 Member 2명이상 소속되어있다면, 값 이 중복됨.
String jpql = "select t from Team t join fetch t.members where t.name = '팀A'" List<Team> teams = em.createQuery(jpql, Team.class).getResultList(); for(Team team : teams) { System.out.println("teamname = " + team.getName() + ", team = " + team); for (Member member : team.getMembers()) { System.out.println(“-> username = " + member.getUsername()+ ", member = " + member); } }
다음과 같은 쿼리를 날리게 되면
TEAM_ID(PK) | NAME | MEMBER_ID(PK) | TEAM_ID(FK) | NAME |
---|---|---|---|---|
1 | 팀A | 1 | 1 | 회원1 |
1 | 팀A | 2 | 1 | 회원2 |
- 다음과 같은 Join테이블이 형성되고, 따라서 Team으로 조회시 팀 A가 두번 조회되는 결과가 나온다.
(2) 페치 조인과 DISTINCT
- SQL의 DISDINCT는 중복된 결과를 제거하는 명령이다.
- 하지만 위의 예시는 회원ID도 다르고 Name또한 다르다.
- JPQL에서 DISTINCT는 2가지 기능을 제공한다.
- SQL에 DISTINCT를 추가해주는 기능
- 애플리케이션에서 엔티티 중복 제거
- 따라서 같은 엔티티가 반환되면 중복을 제거해준다.
4) 페치 조인의 특징과 한계
- 페치 조인 대상에는 별칭을 줄 수 없다.
- 하이버네이트는 가능하지만 가급적 사용x
- 별칭을 사용하여 where문을 통해 엔티티 범위를 한정해서 가져오는 것은 본래 JPQL의 페치 조인 목적에 어긋남.(정합성 문제)
- 한번에 둘 이상의 컬렉션은 페치 조인 할 수 없다.
- Join은 곱연산이기 때문에 잘못된 값이 들어가기도 하고 너무 복잡해짐.
- 컬렉션을 페치 조인하면 페이징 API 사용 불가
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
- 페이징 API를 통해 범위를 지정하여 조회하면 해당 데이터만이 컬렉션의 전부라고 오해할 수 있음.(정합성 문제)
- 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선시함(fetch = FetchType.LAZY)
- 실무에서 글로벌 로딩 전략은 모두 지연로딩
- 최적화가 필요한 곳은 페치 조인 적용(즉시 로딩)
5) 해결책
(1) 별칭 사용
fetch join 대상에는 별칭을 가급적 사용하지 않도록 해야한다.
아래와 같이 작성해서 팀과 연관된 회원 5명 중 1명만 불러온다고 가정한다.
select t from Team t join fetch t.members m where m.username=....;
객체 그래프에서는 team 에 있는 나머지 4명의 회원에 대한 데이터가 누락이 되고 select 해온 t 에서는 1명에 대한 데이터 밖에 없기 때문에 잘못된 사용과 잘못된 결과를 낳을 수 있다.
객체 그래프라는 것은 기본적으로 데이터를 모두 조회하는 것이 좋다.
만약 이렇게 몇개의 데이터만 골라서 가져오고 싶은 경우에는 team 에서 member 를 조회하는 아래 fetch join 방식이 아니라 처음부터 필요한 member 를 조회하도록 짜고, team 의 member 를 모두 조회하는 객체 그래프는 따로 설계한다.
fetch join 을 여러개 연결해서 사용하는 경우에는 별칭을 이용하는 경우가 생길 수 있지만, 그런 상황은 거의 발생하지 않고, JPA 의 설계 목적에 맞게 사용하도록 노력해야한다.
(2) 일대다 fetch join의 페이징
- 절대 사용 x
List<Team> result = em.createQuery("select t from Team t join fetch t.members m ",Team.class) .setFirstResult(0) .setMaxResult(1) .getResultList();
1. 쿼리 뒤집어 사용
List<Member> result =
em.createQuery("select m from Member m join fetch m.team t",Member.class)
.setFirstResult(0)
.setMaxResult(1)
.getResultList();
2. @BatchSize 사용
- fetch join 을 제거
List<Team> result = em.createQuery("select t from Team t",Team.class)
.setFirstResult(0)
.setMaxResult(1)
.getResultList();
Lazy Loading 으로 team 에서 member 를 꺼내 쓸 때 member 를 조회하는 쿼리가 계속 나간다.(N+1문제)
public class Team {
@BatchSize(size=100)
@OneToMany(mappedBy="team")
public List<Member> members = new ArrayList<>();
...
}
- 지정된 size 만큼 SQL의 IN절을 사용해서 조회한다.
- size는 IN절에 올수있는 최대 인자 개수를 말한다.
- team 에서 member 를 가져올 때 100개씩 가져온다.
select
memberlist0_.TEAM_ID as TEAM_ID4_0_1_,
memberlist0_.id as id1_0_1_,
memberlist0_.id as id1_0_0_,
memberlist0_.age as age2_0_0_,
memberlist0_.TEAM_ID as TEAM_ID4_0_0_,
memberlist0_.userName as userName3_0_0_
from
Member memberlist0_
where
memberlist0_.TEAM_ID in (
?, ?
)
혹은 @BatchSize 어노테이션 대신 아래와 같이 persistence.xml 에 설정을 추가해도 된다.
<property name="hibernate.default_batch_fetch_size" value="100" />
6) 페치 조인 정리
- 모든 것을 페치 조인으로 해결할 수는 없음
- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적
3. JPQL - 다형성 쿼리
1) TYPE
조회 대상을 특정 자식으로 한정
JPQL
select i from Item i where type(i) IN (Book, Movie)
SQL
select i from Item i where i.DTYPE in ('B','M')
2) TREAT(JPA 2.1)
자바의 타입 캐스팅과 유사
상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
FROM, WHERE, SELECT(하이버네이트 지원) 사용
JPQL
select i from Item i where treat(i as Book).auter = 'kim'
SQL
/* single table 전략 */ select i.* from Item i where i.DTYPE = 'B' and i.auther = 'kim'
4. JPQL - 엔티티 직접 사용
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
1) 엔티티 직접 사용 - 기본 키 값
(1) 엔티티를 파라미터로 전달
String jpql = “select m from Member m where m = :member”;
List resultList = em.createQuery(jpql)
.setParameter("member", member)
.getResultList();
(2) 식별자를 직접 전달
String jpql = “select m from Member m where m.id = :memberId”;
List resultList = em.createQuery(jpql)
.setParameter("memberId", memberId)
.getResultList();
2) 엔티티 직접 사용 - 외래 키 값
(1) 엔티티를 파라미터로 전달
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();
(2) 식별자를 직접 전달
String qlString = “select m from Member m where m.team.id = :teamId”;
List resultList = em.createQuery(qlString)
.setParameter("teamId", teamId)
.getResultList();
5. JPQL - Named 쿼리
- 미리 정의해서 이름을 부여해두고 사용하는 JPQL
- 정적 쿼리
- 어노테이션, XML에 정의
- 애플리케이션 로딩 시점에 초기화 후 재사용 가능
- 해당 쿼리를 SQL로 변환하여 영속성 컨텍스트에서 보관하고 있음
- 반복 사용시 SQL로 변환 비용이 발생 하지 않음
- 애플리케이션 로딩 시점에 쿼리를 검증 가능
- 문법 오류나 오타를 바로바로 캐치 가능
6. JQPL - 벌크 연산
- 1개의 엔티티를 변경 삭제하는 쿼리를 제외한 모든 변경, 삭제 쿼리
- 여러 엔티티의 변경, 삭제에서 변경된 데이터가 100건이라면 100번의 쿼리 발생
- 벌크 연산 쿼리 한번으로 여러 테이블 로우 변경(엔티티)
- .executeUpdate()
- 결과는 영향받은 엔티티의 수 반환
- UPDATE, DELETE 지원
- INSERT(insert into .. select, 하이버네이트 지원)
1) 주의
- JPQL이기 때문에 실행전 flush가 먼저 실행됨.
- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 날림
- 벌크 연산을 먼저 실행(이후 조회시 영속성 컨텍스트에 담긴게 없으므로 DB로 직접 쿼리를 날려야함 -> 업데이트 동기화)
- 혹은 벌크 연산 수행후 영속성 컨텍스트 초기화(영속성 컨텍스트를 초기화하여 이후 조회시 DB에 직접 쿼리를 날려 데이터를 확인하도록 조작)
'JPA > JPA Basic' 카테고리의 다른 글
[JPA] 9. 객체지향 쿼리 언어1 (0) | 2023.02.03 |
---|---|
[JPA] 8. 값 타입 (0) | 2023.01.31 |
[JPA] 7. 프록시와 연관관계 관리 (0) | 2023.01.31 |
[JPA] 6. 고급 매핑 (0) | 2023.01.29 |
[JPA] 5. 다양한 연관관계 매핑 (0) | 2023.01.29 |
[JPA] 참고. @JoinColumn (0) | 2023.01.29 |
[JPA] 참고. Entity, DAO, DTO, VO (0) | 2023.01.27 |
[JPA] 에러. SQL Error 23505 (0) | 2023.01.27 |