※ 본문은 김영한 선생님의 인프런 '자바 ORM 표준 JPA 프로그래밍 - 기본편' 강의를 듣고 정리한 내용임을 알립니다.
※ JPA가 지원하는 다양한 쿼리 방법들
- JPQL
- JPA Criteria
- QueryDSL
- 네이티브 SQL
- JDBC API 직접 사용 (myBatis, SpringJdbcTemplate)
▶ JPQL (Java Persistence Query Language)
: 가장 단순한 조회 방법
1. JPQL이 생긴 이유
- JPA를 사용하면 Entity 객체를 중심으로 개발
- 검색을 할 때도 테이블이 아닌 Entity 객체를 대상으로 검색해야 함
- 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
- 애플리케이션이 필요한 데이터만 불러오기 위해서는 결국 검색 조건이 포함된 SQL 필요
2. SQL의 특징
- JPA는 SQL을 추상화한 JPQL이라는 객체지향 쿼리 언어 제공
- SQL과 문법 유사 (SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원)
- JPQL은 Entity 객체를 대상으로 쿼리
- SQL을 추상화하기 때문에 특정 데이터베이스에 의존하지 않음
- 한마디로 정의하면 객체지향 SQL
//JPQL로 검색하기
String jpql = "select m from Member m where m.age > 18";
List<Member> result = em.createQuery(jpql, Member.class)
.getResultList();
실행된 SQL
select
m.id as id,
m.age as age,
m.USERNAME as USERNAME,
m.TEAM_ID as TEAM_ID
from
Member m
where
m.age>18
▶ Criteria
- 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
- 동적 쿼리를 쓸 수 있으며 컴파일 단계에서 에러를 잡아낼 수 있음
- 단점 : 너무 복잡하고 실용성이 없음
- Criteria 대신 QueryDSL 사용을 권장
▶ QueryDSL
- 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
- 동적쿼리 작성이 편리함
- JPQL하고 코드가 거의 1:1
- 실무 사용 권장
★ JPQL을 빠삭하게 배워두고 QueryDSL은 reference만 잘 참고해서 사용해도 된다!
▶ Native SQL
- JPA가 제공하는 SQL을 직접 사용하는 기능
- JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능을 사용하기 위해 쓰임
★ 사실, Native로 쿼리를 구성해야 할 때는 SpringJdbcTemplate을 사용하는 것이 좀 더 편리함
▶ JDBC 직접 사용
- JPA를 사용하면서 JDBC Connection 직접 사용 가능
- SpringJdbcTemplate, myBatis 등을 함께 사용할 수도 있음
- 단, 영속성 컨텍스트를 적절한 시점에 강제로 flush해줘야 함
→ JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 flush
=> 95% 정도는 JPQL과 QueryDSL로 작성 / 나머지 정말 안되는 5%는 SpringJdbcTemplate 사용
▶ JPQL 기본 문법
- select m from Member as m where m.age > 18
- Entity와 속성은 대소문자 구분 O (Member, age)
- JPQL 키워드는 대소문자 구분 X (SELECT, from)
- Entity 이름을 사용 / 테이블 이름이 아님 (Member)
- 별칭은 필수 (m) (as는 생략 가능)
- 집합과 정렬(COUNT, SUM, AVG, MAX, MIN) 사용 가능
- GROUP BY, HAVING, ORDER BY 사용 가능
- TypedQuery : 반환 타입이 명확할 때 사용
- Query : 반환 타입이 명확하지 않을 때 사용
- query.getResultList() : 결과가 하나 이상일 때 리스트 반환 (결과가 없으면 빈 리스트 반환)
- query.getSingleResult() : 결과가 정확히 하나일 때 단일 객체 반환 (결과가 없거나 둘 이상이면 Exception 발생)
- 파라미터 바인딩 방법
▶ 프로젝션 (SELECT)
- SELECT절에 조회할 대상을 지정하는 것
- 프로젝션 대상 : Entity, 임베디드 타입, 스칼라 타입(기본 데이터 타입)
- 관계형 데이터베이스는 스칼라 타입만 넣을 수 있음
- SELECT m FROM Member m → Entity 프로젝션
- SELECT m.team FROM Member m → Entity 프로젝션
- SELECT m.address FROM Member m → 임베디드 타입 프로젝션
- SELECT m.username, m.age FROM Member m → 스칼라 타입 프로젝션
- DISTINCT로 중복 제거
※ 프로젝션 - 여러 값을 조회할 때
1. Query 타입으로 조회
2. Object[] 타입으로 조회
3. new 명령어로 조회
→ 단순 값을 DTO로 바로 조회
ex) SELECT new jpql.UserDTO(m.username, m.age) FROM Member m
→ 패키지 명을 포함한 전체 클래스 명 입력
→ 순서와 타입이 일치하는 생성자 필요
▶ 페이징 API
※ 김영한 선생님 曰 "아트의 경지"
※ 사실 중요한 것은 "몇 번 째부터 몇개 가져올래?"
- setFirstResult(int startPosition) : 조회 시작 위치
- setMaxResults(int maxResult) : 조회할 데이터 수
다음은 페이징 API를 사용한 예시 코드이다.
String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setFirstResult(10)
.setMaxResults(20)
.getResultList();
▶ JOIN
- 내부 조인 : 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절을 활용한 JOIN : 대상 필터링 / 연관관계 없는 Entity 외부 JOIN
※ 예시1) 회원과 팀을 조인하면서 팀 이름이 A인 팀만 조인하고 싶을 때
※ 예시2) 회원의 이름과 팀의 이름이 같은 대상을 외부 조인할 때
▶ 서브쿼리
※ 서브쿼리 지원 함수
- [NOT] EXIST (subquery) : 서브쿼리에 결과가 존재하면 참
└> {ALL | ANY | SOME} (subquery) : ALL은 모두 만족하면 참 / ANY, SOME은 하나라도 만족하면 참
- [NOT] IN (subquery) : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
★ JPA 서브쿼리의 한계
- JPA에서는 WHERE, HAVING절에서만 서브쿼리 사용 가능
- 하이버네이트에서 SELECT절 서브쿼리 사용 가능
- FROM절의 서브쿼리는 현재 JPQL에서 불가능
=> JOIN으로 풀 수 있으면 풀어서 해결하는 것이 좋다.
▶ JPQL 타입 표현
- 문자 : 'HELLO', 'She"s'
- 숫자 : 10L(Long), 10D(Double), 10F(Float)
- Boolean : TRUE, FALSE
- ENUM : jpabook.MemberType.Admin (패키지 명 포함)
- Entity 타입 : TYPE(m) = Member (상속관계에서 사용)
▶ 조건식 (CASE 식)
- COALESCE : 하나씩 조회해서 null이 아니면 반환
- NULLIF : 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
※ JPQL에서 사용할 수 있는 기본 함수들
→ CONCAT / SUBSTRING / TRIM / LOWER, UPPER / LENGTH / LOCATE / ABS, SQRT, MOD / SIZE, INDEX
▶ 경로 표현식
: 점을 찍어서 객체 그래프를 탐색하는 것
※ JPQL을 처음 사용할 때 쿼리가 내가 기대했던 대로 안나와서 당황할 수 있음
→ 경로 표현식을 이해하고 가야 한다.
- 상태 필드(state field) : 단순히 값을 저장하기 위한 필드
- 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 Entity
- 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션
1. 경로 표현식 특징
- 상태 필드 : 경로 탐색의 끝, 탐색 X
- 단일 값 연관 경로 : 묵시적 내부 조인(INNER JOIN) 발생, 탐색 O
- 컬렉션 값 연관 경로 : 묵시적 내부 조인 발생, 탐색 X (FROM절에서 명시적 조인을 통해 별칭을 얻으면 가능)
★ 묵시적 내부 조인은 성능 이슈가 있기 때문에 웬만하면 명시적 조인을 사용하자.
2. 명시적 조인 / 묵시적 조인
- 명시적 조인 : join 키워드 직접 사용
- 묵시적 조인 : 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능)
3. 묵시적 조인 주의사항
- 항상 내부 조인
- 컬렉션은 경로 탐색의 끝이므로 명시적 조인을 통해 별칭을 얻어야 함
- 경로 탐색은 주로 SELECT, WHERE절에서 사용하지만 묵시적 조인은 FROM (JOIN)절에 영향을 줌
▶ fetch join
※ 이걸 모르면 실무를 못할 정도로 중요하다.
- SQL 조인의 종류가 아니며, JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 Entity나 컬렉션을 SQL 한번에 함께 조회하는 기능
- join fetch 명령어 사용
1. fetch join 코드 예시
- 회원을 조회하면서 연관된 팀도 SQL 한번에 함께 조회하고 싶을 때
- [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 m만 했는데 SQL에서는 팀(t.*)도 함께 select했음을 알 수 있다.
2. 컬렉션 fetch join
- 일대다 관계에서 사용
- [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'
- 일대다 fetch join은 데이터가 중복되어서 나올 수 있다.
3. fetch join과 distinct
- SQL의 distinct는 중복된 결과를 제거하는 명령
- JPQL의 distinct는 SQL에 distinct 추가 or 애플리케이션에서 Entity 중복 제거
- select distinct t from Team t join fetch t.members where t.name = '팀A'
- JPQL의 distinct는 추가로 애플리케이션에서 중복 제거 시도
- 같은 식별자를 가진 Entity 제거
4. fetch join과 일반 join의 차이
- JPQL은 결과를 반환할 때 연관관계를 고려하지 않고 SELECT절에 지정한 Entity만 조회
- fetch join을 사용할 때만 연관된 Entity를 함께 조회 (즉시 로딩)
- fetch join은 객체 그래프를 SQL 한번에 조회하는 개념
5. fetch join의 특징과 한계
- fetch join 대상에는 별칭을 줄 수 없음
- 둘 이상의 컬렉션은 fetch join할 수 없음
- 컬렉션을 fetch join하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없음
- Entity에 직접 적용하는 글로벌 로딩 전략보다 우선시
=> 실무에서 글로벌 로딩 전략은 모두 지연 로딩으로 설정하고 최적화가 필요한 곳은 fetch join 적용
6. fetch join 정리
- 모든 것을 fetch join으로 해결할 수는 없음
- fetch join은 객체 그래프를 유지할 때 효과적
- 여러 테이블 join해서 Entity가 가진 모양이 아닌 전혀 다른 결과를 내야하면 일반 join 사용
▶ Entity 직접 사용
: JPQL에서 Entity를 직접 사용하면 SQL에서 해당 Entity의 기본 키 값을 사용
▶ 벌크 연산
: 쿼리 한번으로 여러 테이블 row 변경 (Entity)
- executeUpdate()의 결과는 영향받은 Entity 수 반환
- UPDATE, DELETE 지원
- INSERT, SELECT는 하이버네이트 지원
※ 벌크 연산 주의 : 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리
→ 벌크 연산을 먼저 실행하거나, 벌크 연산 수행 후 영속성 컨텍스트 초기화할 것
/* SQL 쿼리들은 국비지원 학원에서 배웠고,
이후 myBatis 환경에서 이런저런 쿼리들을 직접 작성해보면서 충분히 이해하고 있다고 생각했다.
하지만 이번에 JPQL을 공부해보니 이해가 쉽사리 되지 않았고 보충공부가 필요함을 느꼈다.
앞으로 프로젝트를 많이 만들어보면서 직접 부딪혀 보고,
막히는 부분이 있으면 다시 강의를 들어보는 식으로 열심히 반복해야겠다.*/
'JVM > JPA' 카테고리의 다른 글
Querydsl (0) | 2021.05.01 |
---|---|
스프링 데이터 JPA (0) | 2021.04.27 |
값 타입 (0) | 2021.04.16 |
Proxy와 연관관계 관리 (0) | 2021.04.16 |
Entity Mapping (0) | 2021.04.15 |