[JPA] JPQL 기본 개념과 예제
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.
JPA
를 사용하면 엔티티 객체를 중심으로 개발을 해야합니다.
하지만 JPA
만으로 100%의 문제를 해결할 수 없습니다. 🥲
그래서 다양한 쿼리 방법을 지원하는데요
- JPQL
- Querydsl
- 네이티브 SQL
- JDBC API 직접 이용
- MyBatis, SpringJdbcTemplate
그 중에서 JPQL
에 대해서 알아봅시다.
JPQL(Java Persistence Query Language)
순수 JPA
를 사용할 때 가장 단순한 조회 방법은 다음과 같았습니다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("XXX");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
// 조회
em.find(Entity.class, Id);
tx.commit();
그런데 여기에서 조건을 추가하려면 어떻게 해야할까요? 🤔
예를 들어 나이가 20살 이상인 회원을 모두 검색하고 싶다면 어떻게 해야할까요?
순수 JPA의 한계
- 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
- JPA는 테이블이 아닌 엔티티 객체를 대상으로 검색
- 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건을 포함한 SQL이 필요!!
이러한 문제(검색 쿼리)를 해결하기 위해서 JPQL
이 등장했습니다.
JPQL의 특징
JPA
는SQL
을 추상화한JPQL
이라는 객체 지향 쿼리 언어를 제공SQL
을 추상화해서 특정 데이터베이스SQL
에 의존 🙅♂️
SQL
문법와 매우 유사- ANSI 표준:
SELECT
,FROM
,WHERE
,GROUP BY
,HAVING
,JOIN
지원
- ANSI 표준:
- 엔티티 객체를 대상으로 쿼리 생성
SQL
은 데이터베이스 테이블을 대상으로 쿼리 생성
한마디로 JPQL
은 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리 입니다.
이제 JPQL
을 이용해서
나이가 20살 이상인 회원을 모두 검색하는 JPQL
을 작성해 봅시다.
실험
List<Member> result = em.createQuery("select m from Member m" +
" where m.age >= 20", Member.class)
.getResultList();
결과
Hibernate:
/* select
m
from
Member m
where
m.age >= 20 */ select
member0_.id as id1_6_,
member0_.createDate as createda2_6_,
member0_.lastModifiedDate as lastmodi3_6_,
member0_.city as city4_6_,
member0_.street as street5_6_,
member0_.zipcode as zipcode6_6_,
member0_.age as age7_6_,
member0_.name as name8_6_
from
Member member0_
where
member0_.age>=20
JPQL(select m from Member m where m.age >= 20
)이 SQL
로 변환된것을 확인할 수 있습니다.
JPQL 문법
이제 기본적인 문법에 대해서 소개하겠습니다.
select m from Member as m where m.age > 20;
- 엔티티와 속성은 대소문자 구분 🙆♂️
JPQL
키워드는 대소문자 구분 🙅♂️- 엔티티 이름 사용 🙆♂️
- 테이블 이름 🙅♂️
- 별칭은 필수(as 생략 가능)
TypeQuery와 Query
- TypeQuery
- 반환 타입이 명확할 때 사용
- Query
- 반환 타입이 명확하지 않을 때 사용
// 반환 타입이 Member로 명확
TypedQuery<Member> typeQuery = em.createQuery("select m from Member m", Member.class);
// 반환 타입이 명확하지 않음
Query query = em.createQuery("select m.username, m.age from Member m");
결과 조회 API
- query.getResultList()
- 결과가 하나 이상(리스트 반환)
- 결과가 없음(빈 리스트 반환)
- query.getSingleResult
- 결과가 정확히 하나(단일 객체 반환)
- 결과가 없음(
NoResultException
) - 결과가 하나 이상(
NonUniqueResultException
)
// 결과가 없어도 exception 발생 X
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
// 결과가 없으면 exception 발생 O
Member member = em.createQuery("select m from Member m", Member.class)
.getSingleResult();
파라미터 바인딩
이름기준 또는 위치 기준으로 바인딩이 가능합니다.
이름 기준
// 이름기반 바인딩
List<Member> members = em.createQuery("select m from Member m" +
" where m.age < :age", Member.class)
.setParameter("age", 20)
.getResultList();
위치 기준
위치가 바뀌면 장애로 이어질 가능성이 크기 때문에 이름 기준 바인딩을 사용하는 것을 권장
// 위치기반 바인딩
List<Member> members = em.createQuery("select m from Member m" +
" where m.age < :age", Member.class)
.setParameter(1, 20)
.getResultList();
프로젝션
지금까지는 엔티티 전체를 조회하는 방법에 대해서 배웠습니다.
그런데 엔티티 전체가 정말로 필요할까요?
정말로 딱 필요한 데이터만 조회하기 위한 방법으로 프로젝션을 사용하면 됩니다.^^
SELECT
절에 조회할 대상을 지정하는 방법- 프로젝션 대상
- 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자… 기본 데이터 타입)
- 엔티티 프로젝션(영속성 컨텍스트 관리)
select m from Member m
select m.team from Member m
select m.team from Member m
이것보다는select t from Member m join m.team t
으로 하는게 좋음(SQL 예측과 가독성에서 이점)
- 임베디드 타입 프로젝션
select m.address from Member m
- 스칼라 타입 프로젝션
select m.uesrname, m.age from Member m
DISTINCT
로 중복 제거
스칼라 타입 프로젝션은 어떻게 값을 가져오지?🤔
Query 타입으로 조회 후 Object[] 타입으로 변환
tx.begin();
Member member = new Member();
member.setUsername("춘식이");
member.setAge(3);
member.setAddress(new Address("화성시", "화성로", "942"));
em.persist(member);
em.flush();
em.clear();
// Query 타입으로 조회
Query query = em.createQuery("select m.username, m.age from Member m");
// Object -> Object[] 로 변환
List resultList = query.getResultList();
Object o = resultList.get(0);
Object[] result = (Object[]) o;
System.out.println("username="+result[0]); // 춘식이
System.out.println("age="+result[1]); // 3
tx.commit();
new 명령어로 조회
MemberDto 생성
@Data
@AllArgsConstructor
public class MemberDto {
private String username;
private int age;
}
DTO
로 바로 조회가 가능합니다! 👍
패키지 명을 포함한 전체 클래스 명을 입력해야 합니다.
(순서와 타입이 일치하는 생성자 필요)
List<MemberDto> resultList =
em.createQuery(
"select new com.study.MemberDto(m.username, m.age)" +
" from Member m", MemberDto.class)
.getResultList();
페이징 API
- setFirstResult(int startPosition)
- 조회 시작 위치(0부터 시작)
- setMaxResult(int maxResult)
- 조회할 데이터 갯수
List<Member> result = em.createQuery("select m from Member m" +
" order by m.age desc", Member.class)
.setFirstResult(0) // 조회 시작 위치
.setMaxResults(10) // 조회할 데이터 갯수
.getResultList();
Hibernate
가 정말로 좋은게 방언에 맞게 쿼리를 생성한다는 점 입니다.!
MySQL 방언
Hibernate:
/* select
m
from
Member m
order by
m.age desc */ select
member0_.id as id1_0_,
member0_.city as city2_0_,
member0_.street as street3_0_,
member0_.zipcode as zipcode4_0_,
member0_.age as age5_0_,
member0_.team_id as team_id7_0_,
member0_.username as username6_0_
from
Member member0_
order by
member0_.age desc limit ?
Oracle 방언
Hibernate:
/* select
m
from
Member m
order by
m.age desc */ select
member0_.id as id1_0_,
member0_.city as city2_0_,
member0_.street as street3_0_,
member0_.zipcode as zipcode4_0_,
member0_.age as age5_0_,
member0_.team_id as team_id7_0_,
member0_.username as username6_0_
from
Member member0_
order by
member0_.age desc fetch first ? rows only
조인
조인을 모르면 개발을 할수 없다는 말이 있습니다. ㅎㅎㅎ
그만큼 조인은 중요하다는 의미겠죠?
조인에 대해서 알아봅시다.
내부 조인select m from Member m [inner] join m.team t
team
에 데이터 없으면 조회 안됨
String sql = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(sql, Member.class)
.getResultList();
Hibernate:
/* select
m
from
Member m
inner join
m.team t */ select
member0_.id as id1_0_,
member0_.city as city2_0_,
member0_.street as street3_0_,
member0_.zipcode as zipcode4_0_,
member0_.age as age5_0_,
member0_.team_id as team_id7_0_,
member0_.username as username6_0_
from
Member member0_
inner join
Team team1_
on member0_.team_id=team1_.id
inner join Team team1_ on member0_.team_id=team1_.id
이 발생한 것을 확인할 수 있습니다.
외부 조인select m from Member m left [outer] join m.team t
team
에 데이터 없어도 조회 됨(단, team
은 null
)
String sql = "select m from Member m left outer join m.team t";
List<Member> result = em.createQuery(sql, Member.class)
.getResultList();
Hibernate:
/* select
m
from
Member m
left outer join
m.team t */ select
member0_.id as id1_0_,
member0_.city as city2_0_,
member0_.street as street3_0_,
member0_.zipcode as zipcode4_0_,
member0_.age as age5_0_,
member0_.team_id as team_id7_0_,
member0_.username as username6_0_
from
Member member0_
left outer join
Team team1_
on member0_.team_id=team1_.id
left outer join Team team1_ on member0_.team_id=team1_.id
이 발생한 것을 확인할 수 있습니다.
세타 조인select count(m) from Member m, Team t where m.username = t.name
연관관계가 없는 것을 조인하는 SQL
String sql = "select m from Member m, Team t where m.username = t.name";
List<Member> result = em.createQuery(sql, Member.class)
.getResultList();
Hibernate:
/* select
m
from
Member m,
Team t
where
m.username = t.name */ select
member0_.id as id1_0_,
member0_.city as city2_0_,
member0_.street as street3_0_,
member0_.zipcode as zipcode4_0_,
member0_.age as age5_0_,
member0_.team_id as team_id7_0_,
member0_.username as username6_0_
from
Member member0_ cross
join
Team team1_
where
member0_.username=team1_.name
join Team team1_ where member0_.username=team1_.name
이 발생한 것을 확인할 수 있습니다.
ON절 조인(JPA 2.1 지원)
- 조인 대상 필터링
e.g) 회원과 팀을 조인 하면서, 팀 이름이 A인 팀만 조인
JPQL:select m, t from Member m left join m.team t on t.name = ‘A’;
SQL:select m.*, t.* from Member m left join Team t on m.team_id = t.id and t.name = ‘A’;
String sql = "select m, t from Member m left join m.team t on t.name = 'teamA'";
List<Member> result = em.createQuery(sql, Member.class)
.getResultList();
Hibernate:
/* select
m,
t
from
Member m
left join
m.team t
on t.name = 'teamA' */ select
member0_.id as id1_0_0_,
team1_.id as id1_3_1_,
member0_.city as city2_0_0_,
member0_.street as street3_0_0_,
member0_.zipcode as zipcode4_0_0_,
member0_.age as age5_0_0_,
member0_.team_id as team_id7_0_0_,
member0_.username as username6_0_0_,
team1_.name as name2_3_1_
from
Member member0_
left outer join
Team team1_
on member0_.team_id=team1_.id
and (
team1_.name='teamA'
)
- 연관관계 없는 엔티티 외부 조인
e.g) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
JPQL:select m, t from Member m left join Team t on m.username = t.name;
SQL:select m.*, t.* from Member m left join Team t on m.username = t.name;
String sql = "select m, t from Member m left join Team t on m.username = t.name";
List<Query> result = em.createQuery(sql)
.getResultList();
Hibernate:
/* select
m,
t
from
Member m
left join
Team t
on m.username = t.name */ select
member0_.id as id1_0_0_,
team1_.id as id1_3_1_,
member0_.city as city2_0_0_,
member0_.street as street3_0_0_,
member0_.zipcode as zipcode4_0_0_,
member0_.age as age5_0_0_,
member0_.team_id as team_id7_0_0_,
member0_.username as username6_0_0_,
team1_.name as name2_3_1_
from
Member member0_
left outer join
Team team1_
on (
member0_.username=team1_.name
)
서브 쿼리
나이가 평균보다 많은 회원select m from Member m where m.age > (select avg(m2.age) from Member m2);
참고:
메인 쿼리와 서브 쿼리가 서로 관계가 없음(메인 쿼리는 m, 서브 쿼리는 m2 사용)
이러한 방식으로 서브 쿼리를 작성해야 성능이 잘 나온다.
한 건이라도 주문한 고객select m from Member m where where (select count(o) from Order o where m = o.member) > 0;
참고:
메인 쿼리와 서브 쿼리가 서로 관계가 있음(메인 쿼리도 m, 서브 쿼리도 m 사용)
팀A 소속인 회원(exists
)select m from Member m where exists (select t from Team t where t.name = ‘A’);
전체 상품 각각의 재고보다 주문량이 많은 주문들(all
)select o from Order o where o.orderAmount > all (select p.stockAmount from Product p);
어떤 팀이든 팀에 소속된 회원(any
)select m from Member m where m.team = any (select t from Team t);
JPA 서브 쿼리 한계
JPA
는where
,having
,from
절에서 서브 쿼리 사용가능from
절은hibernate 6
부터 지원!(https://in.relation.to/2022/06/24/hibernate-orm-61-features/)
select
절에서도 사용가능(hibernate
에서 지원)
JPQL 타입 표현
- 문자:
‘hello’
,‘he”s’
- 숫자:
10L
,10D
,100F
- Boolean:
TRUE
,FALSE
- ENUM:
jpa.MemberType.Admin
(패키지명 포함)select m.username, 'hello', true from Member m where m.type = jpa.MemberType.Admin;
- 엔티티 타입:
TYPE(i) = Book
(상속 관계에서 사용)select i from Item i where type(i) = Book;
기타 JPQL
- coalesce: 하나씩 조회해서
null
이 아니면 반환
회원 나이가 없으면 내 나이가 어때서~
반환select coalesce(m.age, '내 나이가 어때서~') from Member m;
- null if: 두 값이 같으면
null
, 다르면 첫번째 값 반환
m.age
가 999 이면 null
반환, 나머지는 m.age
반환select null if(m.age, 999) from Member m;
정리
JPQL
은 거의 웬만한 SQL
문법은 지원하니
검색을 통해 찾아보면서 개발하는게 좋다고 생각합니다.🤗
'0+ 스프링 > 0 + 스프링 ORM(JPA)' 카테고리의 다른 글
[JPA] 조건절을 포함한 일대다 페이징 최적화 (0) | 2023.06.07 |
---|---|
[JPA] 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.(값 타입, 엔티티 타입) (0) | 2023.02.27 |
[JPA] CASCADE(영속성 전이)와 고아객체 (0) | 2023.02.24 |
[JPA] JPA가 Entity를 판별하는 방법과 save()의 비밀(entityInformation.isNew(entity)) (0) | 2023.02.22 |
[JPA] 하이버네이트 프록시와 지연로딩(Lazy), 즉시로딩(Eager) (0) | 2023.02.22 |