JPA 프로그래밍 학습 정리 (7) - 객체지향 쿼리언어 (1)
JPQL (Java Persistence Query Language)
테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리이며, SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는 특성을 가지고 있다.
//쿼리 생성
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
* 기존의 SQL 과 다른 점은 테이블의 컬럼명이 아니라 엔티티 객체의 필드명이라는 것이다.
<JPQL>
select m
from Member as m
where m.username = 'kim'
<SQL>
select
member.id as id,
member.age as age,
member.team_id as team,
from
Member member
where member.name = 'kim'
이 외에, Criteria, QueryDSL, 네이티브 SQL, JDBC 직접 사용, SQL 매퍼 프레임워크 사용 등의 객체지향 언어쿼리도 존재한다.
기본 문법과 쿼리 API
JPQL은 SELECT, UPDATE, DELETE문을 사용할 수있다.
SELECT m FROM Member as m where m.username = 'Hello'
- 대소문자 구분
: 엔티티와 속성은 대소문자를 구분한다 - 엔티티 이름
: From 절에 Member는 클래스 명이 아니라 엔티티 명이다 - 별칭은 필수
: JPQL은 별칭을 필수도 사용해야 한다.
TypeQuery, Query
- TypeQuery : 타입을 명확하게 지정할 수 있을때 사용
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = qeury.getResultList();
- Query : 반환 타입을 명확하게 지정할 수 없을때 사용
Query query = em.createQuery("SELECT m.username, m.age from Member m");
List resultList = query.getResultList();
for(Object o : resultList) {
Object[] result = (Object[]) o; //결과가 둘 이상이면 Object[] 반환
}
해당 예제 처럼 조회 대상이 둘 이상이면 Object[]를 반환하고 하나면 Object를 반환한다.
결과 조회
- query.getResultList() : 결과를 예제로 반환한다.
- query.getSingleResult() : 결과가 정확히 하나일 때 사용한다. 아니면 에러 발생
파라미터 바인딩
- 이름 기준 파라미터
String usernameParam = "User1";
TypedQuery<Member> query =
em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class)
.setParameter("username", usernameParam)
.getResultList();
:변수 로 기준 파라미터를 정의하고 setParameter함수를 사용하여 바운딩 해준다.
프로젝션
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라고 한다.
- 엔티티 프로젝션
SELECT m FROM member m 과 같이 엔티티를 프로젝션으로 갖는 것
따라서, 조회한 엔티티는 영속성 컨텍스트에서 관리된다. - 임베디드 타입 프로젝션
임베티드 타입으로 정의한 값 타입을 조회하는 것
따라서, 영속성 컨텍스트에서 관리되지 않는다. - 스칼라 타입 프로젝션
숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라고 한다. - NEW 명령어
새로운 DTO를 정의해서 의미 있는 객체로 변환해서 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class UserDTO {
private String username;
private int age;
public UserDTO(String username, int age) {
this.username = username;
this.age = age;
}
//...
}
TypedQuery<UserDTO> query =
em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age)
FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
|
cs |
- 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
- 순서와 타입이 일치하는 생성자가 필요하다
페이징 API
- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
- setMaxResults(int maxResult) : 조회할 데이터 수
집합(COUNT, MAX, MIN, AVG, SUM, GROUP BY, HAVING), 정렬(ORDER BY)는 SQL과 동일
JPQL 조인
내부조인
1
2
3
4
5
6
7
|
String teamName = "TeamA";
String query = "SELECT m FROM Member m INNER JOIN m.team t"
+ "WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class);
.setParameter("teamName", teamName)
.getResultList();
|
cs |
SQL과 약간 다른 부분은 연관 필드를 사용한다는 것이다. 여기서 m.team이 연관 필드이다.
- Member m JOIN m.team t : 회원이 가지고 있는 연관 필드로 팀과 조인한다. 조인한 팀에는 t라는 별칭을 주었다.
외부 조인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t
[SQL]
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M LEFT JOIN TEAM T ON M.TEAM_ID = T.ID
WHERE
T.NAME = ?
|
cs |
sql과 기능상 같다.
컬렉션 조인
? : N (?대다) 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라고 한다.
SELECT t, m From Team t LEFT JOIN t.members m
팀이 보유한 회원 목록을 컬렉션 값 연관 필드로 외부 조인했다.
세타 조인
WHERE 절을 사용해서 세타 조인을 할 수 있다. 세타조인은 내부 조인만 지원한다.
1
2
3
4
5
6
7
8
9
10
11
|
//JPQL
select count(m) from Member m, Team t
where m.username = t.name
//SQL
SELECT COUNT(M.ID)
FROM
MEMBER M CROSS JOIN TEAM T
WHERE
M.USERNAME = T.NAME
|
cs |
JOIN ON 절
보통 ON절은 외부 조인에서만 사용한다.
1
2
3
4
5
6
7
8
|
//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'
|
cs |
페치 조인
JPQL에서 성능 최적화를 위해 제공하는 기능이며 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이다.
join fetch 명령어로 사용할 수 있다.
엔티티 페치 조인
1
2
3
4
5
6
7
8
9
|
select m
from Member m join jetch m.team // m.team 에는 별칭을 사용할 수 없다.
[SQL]
SELECT
M.*, T.*
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID = T.ID
|
cs |
해당 조인을 사용하면 회원에 연관된 팀도 한번에 조회된다.
1
2
3
4
5
6
7
8
9
|
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();
for(Member member : members) {
//페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 발생 안 함
member.getUsername();
member.getTeam().name();
}
|
cs |
페치조인이 지연로딩보다 우선순위를 갖기때문에, Team의 정보를 사용할 때 지연로딩이 발생하지 않는다.
컬렉션 페치 조인
select t
from Team t join fetch t.members
where t.name = 'TeamA'
[SQL]
SELECT
T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = 'teamA'
? : N 처럼 다 관계를 향한 컬렉션 페치 조인을 사용하게 되면, 팀에 대한 정보를 불러오더라도 회원에 개수만큼 더 많은 건수를 조회하게 된다.
📌 이렇듯, 일대다 조인은 결과가 증가할 수 있지만 일대일, 다대일 조인은 결과가 증가하지 않는다.
페치 조인의 특징과 한계 :
페치 조인을 사용하면 SQL 호출 횟수를 줄여 성능을 최적화 할 수있다. 그리고 페치 조인은 글로벌 로딩 전략보다 우선순위를 가지고 있다. 하지만 전체적으로 봤을때 사용하지 않는 엔티티를 자주 로딩하므로 오히려 성능에 악형향을 미칠 수 있다. 따라서 글로벌 로딩 전략은 될 수 있으면 지연로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이라 할 수 있다.
- 페치 조인 대상에는 별칭을 줄 수 없다.
그래서 SELECT, WHERE 절, 서브 쿼리에 페치 조인 대상을 사용할 수 없다. - 둘 이상의 컬렉션을 페치할 수 없다.
- 페이징 API와 같이 사용할 수 없다.
페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다. 하지만 여러 테이블을 조인해서 필요한 필드들만 조회해서 DTO를 만들어야 되는 경우는 이것이 더 효과적일 수 있다.
경로 표현식
.(점)을 찍어 객체 그래프를 탐색하는 것이다.
- 상태 필드(state field) : 단순히 값을 저장하기 위한 필드
- 연관 필드(association field) : 연관관계를 위한 필드, 임베디드 타입 포함
- 단일 값 연관 필드 : @XtoOne, 대상이 엔티티
- 컬렉션 값 연관 필드 : @XtoMany, 대상이 컬렉션
1
2
3
4
5
6
7
8
9
10
11
|
@Entity
public class Member {
private String username; // 상태필드
private String age; // 상태필드
@ManyToOne(...)
private Team team; // 연관 필드(단일 값 연관필드)
@OneToMany(..)
private List<Order> orders; // 연관 필드(컬렉션 값 연관필드)
}
|
cs |
- 상태 필드 : 경로 탐색의 끝. 더는 탐색 불가.
예) t.username, t.age - 단일 값 연관 필드 : 묵시적 내부조인이 일어난다. 계속 탐색 가능
예) m.team - 컬렉션 값 연관 필드 : 묵시적 내부조인이 일어난다. FROM절에서 조인을 통해 별칭을 얻어야만 탐색 가능
예) m.orders
📌
* 명시적 조인 : JOIN 을 직접 적어주는 것
* 묵시적 조인 : 내부조인이 묵시적으로 일어나는 것
상태 필드 경로 탐색
select m.username, m.age from Member m
[SQL]
select m.name, m.age
from Member m
단일 값 연관 경로 탐색
select o.member from Order o
[SQL]
select m.*
from Orders o
inner join Member m on o.member_id = m.id
select o.member.team
from Order o
where o.product.name = 'productA' and o.address.city = 'jinju'
[SQL]
select t.*
from Orders o
inner join Member m on o.member_id = m.id
inner join Team t on m.team_id = t.id
inner join Product p on o.product_id = p.id
where p.name = 'productA' and o.city = 'jinji'
Order엔티티에서 부터 총 3개의 연관관계 탐색에 의한 조인이 일어난다. address 임베디드 타입은 order 테이블에 이미 포함되어 있어서 조인이 발생하지 않는다.
컬렉션 값 연관 경로 탐색
📌 컬렉션 값에서 경로 탐색을 시도하는 것이 가장 흔한 실수이다.
→ 경로 탐색을 하고 싶으면 조인을 사용해서 별칭을 얻어야 한다.
select m.username from Team t join t.members m
이렇듯 m 이라는 별칭을 획득하면 다시 탐색을 할 수 있다.
서브 쿼리
JPQL에서는 서브쿼리를 WHERE, HAVING 절(조건절)에서만 사용할 수 있다.
- EXISTS : 한개라도 존재하면
- ALL | ANY | SOME : 모두 만족하거나 || 하나라도 만족하면
- IN : 하나라도 같은 것이 있으면
연산자 우선 순위
- 경로 탐색 연산(.)
- 수학 연산(+,-,*,/)
- 비교 연산(거의 대부분)
- 논리 연산(AND, OR, NOT)
컬렉션 식
컬렉션에서만 사용하는 특별한 기능이 있다.
- IS EMPTY : 컬렉션에 값이 비었으면 참
- MEMBER [OF] : 엔티티나 값이 컬렉션에 포함되어 있으면 참
CASE 식
- 기본 CASE : WHEN 다음 조건절에 조건식이 나오는 경우
- 심플 CASE : WHEN 다음 조건절에 스칼라식이 나오는 경우
- COALESCE : 차례대로 조회해서 null이 아니면 반환
- NULLIF : 두 값이 같으면 NULL을 반환하고 다르면 첫 번째 값을 반환
//기본 CASE
select
case when m.age <= 10 then 'a'
else 'b'
end
from Member m
//심플 CASE
select
case t.name when 'a' then 'a'
when 'b' then 'b'
else 'c'
end
from Team t
//COALESCE (m.username이 null이면 a를 반환
select coalesce(m.username, 'a') from Member m
//nullif (m.username가 b면 null을 반환하고 아니면 m.username반환)
select NULLIF(m.username, 'b') from Member m
다형성 쿼리
부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회한다.
// 단일 테이블 전략에서의 SQL
select i from Item i
[SQL]
SELECT
i.*,
b.*,
a.*,
m.*
FROM
Item i
left outer join
Book b on i.ITEM_ID = b.ITEM_ID
left outer join
Album a on i.ITEM_ID = a.ITEM_ID
left outer join
Movie m on i.ITEM_ID = m.ITEM_ID
TYPE
조회 대상을 특정 자식의 타입으로 한정하여 사용
select i from Item i
where type(i) IN (BOOK, Movie)
[SQL]
SELECt i FROM Item i
where i.DTYPE in ('B', 'M')
엔티티 직접 사용
기본 키 값
엔티티를 직접 사용하면 JPQL이 SQL로 변환될 때 해당 엔티티의 기본 키를 사용한다.
select count(m.id) from Member m
select count(m) from Member m
같은 의미이다
외래 키 값
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();
해당 쿼리는 팀의 외래 키 값인 1L값이 매핑된다.
String qlSring = "select m from Member m where m.team.id = :teamId";
List resultList = em.createQuery(qlString)
.setParameter("teamId", 1L)
.getResultList();
해당 JPQL과 같은 원리로 동작한다.
Named 쿼리 : 정적 쿼리
- 동적 쿼리 : JPQL로 문자를 완성해서 직접 넘기는 쿼리
- 정적 쿼리 : 미리 정의한 쿼리 Named쿼리라 부르며 한 번 정의하면 변경할 수 없는 정적인 쿼리
✔️ Named 쿼리는 애플리케이션 로딩 시점에 미리 파싱하고 문법을 체크한다. 따라서 오류를 빨리 확인할 수 있고, 성능상 이점도 있다. 데이터베이스의 조회 성능 최적화에도 도움이 된다.
Named 어노테이션 정의
1
2
3
4
5
6
7
8
9
10
11
|
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username")
public class Member {...}
List<Member> resultList = em.createNamedQuery("Member.findByUsername",
Member.class)
.setParameter("username", "Member1")
.getResultList();
|
cs |
@NamedQuery에 미리 쿼리를 정의하고 createNamedQuery함수를 통해 사용한다.
2개 이상의 쿼리 정의
1
2
3
4
5
6
7
8
9
10
|
@Entity
@NamedQueries({
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"),
@NamedQuery(
name = "Member.count",
query = "select count(m) from Member m")
})
public class Member {...}
|
cs |
Named 쿼리를 XML에 정의
<named-query name = "Member.findByUsername">
<query>
select m
from Member m
where m.username = :username
</query>
</named-query>
해당 파일을 META-INF/orm.xml 에 생성했다면 별도로 작업을 해주지 않아도 된다.
하지만 새로운 xml 파일(ormMember.xml)에 정의 했다면 persistence.xml에 설정 정보를 추가해주어야 한다.
<persistence-unit name = "jpabook">
<mapping-file>META-INF/ormMember.xml</mapping-file>
...
어노테이션과 XML에 모두 정의했다면, XML파일이 우선권을 갖게 된다.
https://taehoon9393.tistory.com/371
JPA 프로그래밍 학습 정리 (7) - 객체지향 쿼리언어 (2)
QueryDSL : 데이터를 조회하는 데 기능이 특화되어 있다. ✔️ 해당 기능을 사용하기 위해서는 의존성과 라이브러리를 추가 해야한다. pom.xml com.querydsl querydsl-apt 4.4.0 provided com.querydsl querydsl-jpa 4.4.0
taehoon9393.tistory.com