JPA는 다양한 쿼리 방법을 지원
JPQL는 가장 단순한 조회 방법입니다.
JPA를 사용하면 엔티티 객체를 중심으로 개발
문제는 검색 쿼리인데 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함한 SQL이 필요
JPA는 SQL을 추상화한 JPQL이라는 객체지향 쿼리 언어 제공
SQL과 문법 유사
select, from, where, group by, having, join 지원
JPQL은 엔티티 객체를 대상으로 쿼리
SQL은 데이터베이스 테이블을 대상으로 쿼리
SQL을 추상화해서 특정 데이터베이스 SQL에 의존x
JPQL을 한마디로 정의하면 객체 지향 SQL
실무에서는 JPA를 사용할 때 동적 쿼리를 사용할 일이 많은데 그럴 때 QueryDSL을 사용하면 된다.
JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리를 하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
JPQL은 결국 SQL로 변환된다.
Member, age
select, from, where
as는 생략 가능
TypeQuery
반환 타입이 명확할 때 사용
Query
반환 타입이 명확하지 않을 때 사용
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 -> 스칼라 타입 프로젝션
DISTINCT로 중복 제거
SELECT m.username, m.age FROM Member m
- 단순 값을 DTO로 바로 조회
SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM
Member m- 패키지 명을 포함한 전체 클래스명 입력
- 순서와 타입이 일치하는 생성자 필요
setFirstResult(int startPosition) : 조회 시작 위치
(0부터 시작)setMaxResults(int maxResult) : 조회할 데이터 수
하이버네이트6 부터는 FROM 절의 서브쿼리를 지원합니다.
form 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
실무에서는 묵시적 조인을 사용하지말고 명시적 조인을 사용하는것이 좋다.
명시적 조인 : join 키워드 직접 사용
select m from Member m join m.team t
묵시적 조인 : 경로 표현식에 의헤 묵시적으로 SQL 조인 발생 (내부조인만 가능)
select m.team from Member m
성능 최적화
를 위해 제공하는 기능한번에 함께 조회
하는 기능JPA를 사용하다 보면 의도하지 않았지만 여러 번의 select 문이 순식간에 여러 개가 나가는 현상을 본 적이 있을 것이다. 이러한 현상을 N+1문제라고 부른다.
N+1 문제란?
연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 현상
발생 이유
N+1 문제가 발생하는 이유는 JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 Fetch 전략을 참고하지 않고 오직 JPQL 자체만을 사용한다. 즉, 아래와 같은 순서로 동작한다.
- Fetch 전략이 즉시 로딩인 경우
- findAll()을 한 순간 select t from Team t 이라는 JPQL 구문이 생성되고 해당 구문을 분석한 select * from team 이라는 SQL이 생성되어 실행된다. ( SQL 로그 중 Hibernate: select team0.id as id1_0, team0.name as name2_0 from team team0_ 부분 )
- DB의 결과를 받아 team 엔티티의 인스턴스들을 생성한다.
- team과 연관되어 있는 user 도 로딩을 해야 한다.
- 영속성 컨텍스트에서 연관된 user가 있는지 확인한다.
- 영속성 컨텍스트에 없다면 2에서 만들어진 team 인스턴스들 개수에 맞게 select * from user where team_id = ? 이라는 SQL 구문이 생성된다. ( N+1 발생 )
- Fetch 전략이 지연 로딩인 경우
- findAll()을 한 순간 select t from Team t 이라는 JPQL 구문이 생성되고 해당 구문을 분석한 select * from team 이라는 SQL이 생성되어 실행된다. ( SQL 로그 중 Hibernate: select team0.id as id1_0, team0.name as name2_0 from team team0_ 부분 )
- DB의 결과를 받아 team 엔티티의 인스턴스들을 생성한다.
- 코드 중에서 team 의 user 객체를 사용하려고 하는 시점에 영속성 컨텍스트에서 연관된 user가 있는지 확인한다
- 영속성 컨텍스트에 없다면 2에서 만들어진 team 인스턴스들 개수에 맞게 select * from user where team_id = ? 이라는 SQL 구문이 생성된다. ( N+1 발생 )
N + 1 문제가 발생할 경우 fetch join
으로 해결해야 한다.
JPQL을 사용하여 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 같이 가져오게 하는 방법이다.
// N +1 문제를 해결하기 위해서는 join fetch를 사용한다.
// 연관된 데이터를 가지고 오려면 join을 해야하고
// fetch는 한번에 가지고 온다는 것이다.
String query = "select m From Member m join fetch m.team";
// 여기서 List에 담기는 값은 프록시가 아니라 엔티티의 값이다.
List<Member> resultList =
entityManager.createQuery(query, Member.class).getResultList();
여러번 실행하지 않고 한번에 실행되는 것을 볼 수 있다.
String query = "select t From Team t join fetch t.members";
// 여기서 List에 담기는 값은 프록시가 아니라 엔티티의 값이다.
List<Team> resultList = entityManager.createQuery(query, Team.class).getResultList();
for (Team team : resultList) {
log.info("team : " + team.getName() + "\n members : " +team.getMembers().toString());
}
이렇게 실행하면 2번 실행되는 것을 볼 수 있는데 이유는 다음과 같다.
지금 팀A에 member가 2명이 있기때문에 일대다
인데 멤버가 2명이기 때문에 2번 뜨는 것이다.
- SQL에 DISTINCT를 추가
- 애플리케이션에서 엔티티 중복 제거
페치 조인 대상에는 별칭을 줄 수 없다.
하이버네이트는 가능, 가급적 사용X
둘 이상의 컬렉션은 페치 조인 할 수 없다.
컬렉션을 페치 조인하면 페이징 API(setFirstResult,
setMaxResults)를 사용할 수 없다.
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
@OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
실무에서 글로벌 로딩 전략은 모두 지연 로딩
최적화가 필요한 곳은 페치 조인 적용
재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면
- 재고가 10개 미만인 상품을 리스트로 조회한다.
- 상품 엔티티의 가격을 10% 증가한다.
- 트랜잭션 커밋 시점에 변경감지가 동작한다.
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리