본 포스팅은 인프런 - 자바 ORM 표준 JPA 프로그래밍 (기본편) 을 강의를 바탕으로 공부하고 정리한 글입니다.
JPQL이란?
- 객체지향 쿼리 언어
- 테이블을 대상으로 쿼리하는 것이 아닌 엔티티 객체를 대상으로 쿼리한다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다는 장점이 있다.
- JPQL도 결국 SQL로 변환되어 실행된다.
JPQL 기본 문법
select, update, delete
select m from Member as m where m.age > 18
- SQL 문법과 동일하다.
- 엔티티, 속성은 대소문자를 구분한다. (Member, age)
- JPQL 키워드는 대소문자를 구분하지 않는다. (SELECT, FROM, WHERE)
- 테이블 이름이 아닌 엔티티 이름을 사용한다.
- 별칭을 필수로 사용한다. (m) (as 생략 가능)
집합과 정렬
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
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class); // 두번째 인자로 타입 지정
Query query = em.createQuery("select m.username, m.age from Member m"); // String, int 둘 중 어떤 타입으로 지정 불가능
- TypeQuery : 반환 타입이 명확할 때 사용
- Query : 반환 타입이 명확하지 않을 때 사용
결과 조회 API
List<Member> result = em.createQuery("select m from Member m", Member.class)
.getResultList();
Member result = em.createQuery("select m from Member m where m.id=1", Member.class)
.getSingleResult();
- query.getResultList()
- 결과가 1개 이상일 때 사용
- 리스트 반환
- 결과가 없으면 빈 리스트를 반환
- query.getSingleResult()
- 결과가 정확히 1개일 때 사용
- 단일 객체 반환
- 결과가 없으면 javax.persistence.NoResultException
- 결과가 둘 이상이면 javax.persistence.NonUniqueResultException
파라미터 바인딩
Member result = em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
- query.setParameter() : ":parameter"로 기준을 지정한뒤, setParameter() 메서드 체이닝으로 값을 지정
여러 값 조회
// 방법1
List result = em.createQuery("select m.username, m.age from Member m")
.getResultList();
Object o = result.get(0);
Object[] ol = (Object[]) o;
// 방법2
List<Object[]> result = em.createQuery("select m.username, m.age from Member m")
.getResultList();
Object[] ol = result.get(0);
// 방법3
List<MemberDto> result = em.createQuery("select new jpql.MemberDto(m.username, m.age) from Member m", MemberDto.class)
.getResultList();
public class MemberDto {
private String username;
private int age;
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
- Query 타입으로 조회
- Query[] 타입으로 조회
- new 명령어로 조회
- 단순 값을 DTO로 바로 조회
- ex) select new jpa.book.jpql.UserDto(m.username, m.age) from Member m
- 패키지 명을 포함한 전체 클래스명을 입력해줘야 한다.
- 순서와 타입이 일치하는 생성자가 필요하다.
프로젝션
-- 엔티티 프로젝션
select m from Member m
select m.team from Member m
-- 임베디드 타입 프로젝션
select m.address form member m
-- 스칼라 타입 프로젝션
select distinct m.username, m.age from Member m
- 프로젝션은 SELECT 절에서 조회할 대상을 지정하는 것을 말한다.
- 프로젝션의 대상 : 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)
- DISTINCT로 중복을 제거할 수 있다.
- 엔티티 프로젝션으로 조회된 모든 엔티티는 영속성 컨텍스트로 관리된다.
조건식 (CASE 식)
/* 기본 CASE 식 */
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
/* 단순 CASE 식 */
select
case t.name
when '팀A' then '인센티브110%'
when '팀B' then '인센티브120%'
else '인센티브105%'
end
from Team t
페이징
JPA는 다음 두 API를 사용해 페이징 처리를 할 수 있다.
setFirstResult(int startPosition) | 조회 시작 위치 |
setMaxResults(int maxResult) | 조회 할 데이터의 수 |
거두절미하고 바로 코드로 알아보자.
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(1) // 1번째 부터
.setMaxResults(10) // 10개를 가져오겠다
.getResultList();
System.out.println("result.size = " + result.size());
for (Member member : result) {
System.out.println("member = " member);
}
- order by m.age desc : 데이터를 나이 내림차순으로 정렬한다.
- setFirstResult(1) : 1번째 데이터부터 조회해온다.
- setMaxReuslt(10) : 10개의 데이터를 조회해온다.
Hibernate:
/* select
m
from
Member m
order by
m.age desc */ select
... 생략
from
Member member0_
order by
member0_.age desc limit ? offset ?
result.size() = 10
Member{id=51, username='member50', age=50}
Member{id=50, username='member49', age=49}
Member{id=49, username='member48', age=48}
Member{id=48, username='member47', age=47}
Member{id=47, username='member46', age=46}
Member{id=46, username='member45', age=45}
Member{id=45, username='member44', age=44}
Member{id=44, username='member43', age=43}
Member{id=43, username='member42', age=42}
Member{id=42, username='member41', age=41}
출력을 보면 나이 내림차순으로 조회하고, 0번째 데이터부터 10개의 데이터를 정상적으로 가져온 것을 확인할 수 있다.
쿼리문을 보면 현재 DB를 H2로 설정했기 때문에 H2에 맞는 SQL로 작성되었다.
만약 다른 DB를 사용하고 싶다면 방언을 변경 해주면 된다.
💡 방언 변경방법
📁 META-INF/persistance.xml
// H2 설정
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
// Oracle 설정
<property name="hibernate.dialect" value="org.hibernate.dialect.Oracle8iDialect"/>
- dialect 설정만 변경해주면 JPA가 알아서 설정해준 DB에 맞는 SQL을 날려준다.
- ex) Oracle
조인
// 내부 조인
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
- 내부 조인 : 조인 대상(team)이 없으면 데이터(member)를 출력하지 않는다.
- 외부 조인 : 조인 대상(team)이 없어도 데이터(member)를 출력한다.
- 세타 조인 : 막 쿼리
내부(이너) 조인
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);
Team team = new Team();
team.setName("teamA");
team.addMember(member); // 연관관계 편의 메서드 호출
em.persist(team);
em.flush();
em.clear();
String query = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
Hibernate:
/* select
m
from
Member m
inner join
m.team t */ select
... 생략
from
Member member0_
inner join
Team team1_
on member0_.team_id=team1_.team_id limit ?
Hibernate:
select
... 생략
from
Team team0_
where
team0_.team_id=?
실행해보니 사용하지도 않았는데 team을 조회하는 쿼리가 하나 더 나간 것을 알 수 있다.
이러한 이유 때문에 @ManyToOne은 항상 Lazy(지연로딩)로 설정해줘야 한다는 주의점이 있다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
inner 키워드는 생략이 가능하다.
String query = "select m from Member m join m.team t";
외부 조인
String query = "select m from Member m left outer join m.team t";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
/* select
m
from
Member m
left join
m.team t */ select
... 생략
from
Member member0_
left outer join
Team team1_
on member0_.team_id=team1_.team_id limit ?
left outer join된 것을 확인할 수 있다.
outer 키워드는 생략이 가능하다.
String query = "select m from Member m left join m.team t";
세터 조인
String query = "select m from Member m, Team t where m.username = t.name";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
/* select
m
from
Member m,
Team t
where
m.username = t.name */ select
... 생략
from
Member member0_ cross
join
Team team1_
where
member0_.username=team1_.name
cross join 된 것을 확인할 수 있다.
조인 - ON절
- JPA는 ON절을 활용한 조인을 지원한다. (JPA 2.1부터 지원)
- ON절을 이용해 조인 대상을 필터링할 수 있다.
- ON절을 이용해 연관관계가 없는 엔티티를 외부 조인할 수 있다. (하이버네이트 5.1부터 지원)
👉🏻 조인 대상 필터링
ex) 회원과 팀을 조인하는데, 팀 이름이 A인 팀만 조인하려는 경우
select m, t from Member m left join m.team t on t.name = 'A'
👉🏻 연관관계가 없는 엔티티 외부 조인
ex) 회원의 이름과 팀의 의름이 같은 대상을 외부 조인하려는 경우
select m, t from Member m left join Team t on m.username = t.name
서브 쿼리
JPQL도 SQL처럼 서브 쿼리를 지원한다.
서브 쿼리는 쿼리 안에서 또 다른 쿼리를 말한다.
다음은 서브 쿼리를 사용한 예시이다.
/* 나이가 평균보다 많은 회원을 조회 */
select m from Member m
where m.age > (select avg(m2.age) from Memver m2)
예시를 보면 메인 쿼리와 서브 쿼리는 서로 전혀 관계가 없도록 한 것을 알 수 있다.
메인 쿼리에서는 m을 사용하지만, 서브 쿼리에서는 m2로 따로 정의해서 둘이 전혀 연관이 없도록 작성되어 있다.
이렇게 서브 쿼리를 짜야 성능이 잘 나온다.
/* 한 건이라도 주문한 고객을 조회 */
select m from Member m
where (select count(o) from Order o where m = o.member) > o
위 예제에서는 서브 쿼리에서 메인 쿼리에서 사용하는 m을 끌고와 사용하고 있다.
이렇다면 성능 문제가 있을 수 있다.
서브 쿼리 지원 함수
함수 | 설명 |
EXISTS | 서브 쿼리에 결과가 존재하면 참 |
ALL | 모두 만족하면 참 |
ANY, SOME | 조건을 하나라도 만족하면 참 |
IN | 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참 |
예제를 통해 서브 쿼리 지원 함수 사용법을 알아보자.
/* 팀A 소속인 회원 조회 */
select m form Member m
where EXISTS (select t from m.team t where t.name = 'A')
/* 전체 상품 각각의 재고보다 주문량이 많은 주문들 */
select o form Order o
where o.orderAmount > ALL (select p.stockAmount from Product p)
/* 어떤 팀이든 팀에 소속된 회원 */
select m from Member m
where m.team = ANY (select t from Team t)
JPA 서브 쿼리의 한계점
JPA의 서브 쿼리는 SQL의 서브 쿼리와 똑같지만 다음과 같은 한계점이 있다.
- JPA 표준 스펙에서는 WHERE, HAVING 절에서만 서브 쿼리 사용이 가능하다.
- JPA 구현체중 하이버네이트에서는 SELECT절에서도 서브 쿼리 사용을 지원한다.
→ 따라서 사용할 구현체의 스펙을 잘 알아보고 사용해야 한다. - FROM 절의 서브 쿼리는 현재 JPQL에서 불가능하다.
→ 조인으로 풀 수 있으면 조인으로 풀어서 해결한다.
/* select절에 서브 쿼리
* 하이버네이트에서만 사용 가능
*/
select (select avg(m1.age) from Member m1) as avgAge
from Member m
/* from절에 서브쿼리
* JPQL에서 불가능
*/
select mm.username, mm.age
from (select m.username, m.age from Member m) as mm
'🗺 RoadMap > Jpa' 카테고리의 다른 글
[JPA] #17 JPQL : 중급 문법 (0) | 2023.07.04 |
---|---|
[JPA] OSIV (0) | 2022.12.27 |
[JPA] #15 JPA가 지원하는 쿼리 방법 (JPQL, Criteria, QueryDsl) (0) | 2022.10.29 |
[JPA] #14 값 타입 (0) | 2022.10.19 |
[JPA] #13 영속성 전이(CASECADE)와 고아 객체 (0) | 2022.10.18 |
[JPA] #12 즉시 로딩(LAZY)과 지연 로딩(EAGER) (0) | 2022.10.18 |
[JPA] #11 프록시 (0) | 2022.10.18 |
[JPA] #10 상속관계 매핑(@Inheritance), 매핑 정보 상속@MappedSuperclass) (0) | 2022.10.17 |