자바 ORM 표준 JPA 프로그래밍 | 김영한 - 교보문고
자바 ORM 표준 JPA 프로그래밍 | 자바 ORM 표준 JPA는 SQL 작성 없이 객체를 데이터베이스에 직접 저장할 수 있게 도와주고, 객체와 관계형 데이터베이스의 차이도 중간에서 해결해준다. 이 책은 JPA
product.kyobobook.co.kr
1. 기본 문법과 쿼리 API
SELECT 문
SELECT m FROM Member AS m where m.username = 'Hello'
- 대소문자 구분
- 엔티티와 속성은 대소문자를 구분한다.
- Member와 username은 대소문자를 구분
- SELECT, FROM, AS 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
- 엔티티와 속성은 대소문자를 구분한다.
- 엔티티 이름
- JPQL에서 사용한 Member는 클래스명이 아니라 엔티티명이다.
@Entity(name="XXX")
로 엔티티명을 지정할 수 있다- 엔티티명을 지정하지 않으면 클래스명을 기본값으로 사용한다.
- 별칭은 필수
m
과 같은 별칭을 필수로 사용해야 하며, AS는 생략이 가능하다.
TypeQuery, Query
- 쿼리 객체
- 반환 타입을 명확하게 지정할 수 있으면 TypeQuery
- 반환 타입을 명확하게 지정할 수 없으면 Query
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = query.getResultList();
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList2 = query.getResultList();
for (Object o : resultList) {
Object[] result = (Object[]) o; // 결과가 둘 이상이면 Object[] 반환
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
}
- 조회대상이 회원이름(String)과 나이(Integer) 이므로 조회 타입이 명확하지 않아 Query 객체 사용
em.createQuery()
의 두 번째 파라미터에 반환할 타입을 지정하면 TypeQuery를 반환하고 지정하지 않으면 Query를 반환- 타입을 변환할 필요가 없는 TypeQuery를 사용하는 것을 추천
결과 조회
query.getResultList()
: 결과를 컬렉션으로 반환하며, 결과가 없으면 빈 컬렉션을 반환한다.query.getSingleResult()
: 결과가 정확히 하나일 때 사용하며, 객체 하나를 반환- 결과가 없거나, 1개보다 많으면 예외가 발생한다.
2. 파라미터 바인딩
이름 기준
- 파라미터 이름으로 구분하는 방법으로 이름 기준 파라미터는 앞에
:
를 사용한다.
String usernameParam = "User1";
TypeQuery<Member> query =
em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class);
.setParameter("username", usernameParam);
:username
이라는 파라미터 정의.setParameter()
에서 username이라는 이름으로 파라미터 바인딩
위치 기준
?
다음 에 위치값을 주면 되고, 위치 값은 1부터 시작한다.
List<member> resultList =
em.createQuery("SELECT m FROM Member m WHERE m.username = ?1", Member.class)
.setParameter(1, usernameParam)
.getResultList();
위치기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.
파라미터 바인딩 방식을 사용하지 않으면 SQL 인젝션 공격을 당할 수도 있다.
파라미터 바인딩 방식은 성능 향상에도 도움이 되므로 선택이 아닌 필수로 사용하자.
3. 프로젝션
- SELECT절에 조회할 대상을 지정하는 것을 프로젝션이라고 한다.
SELECT {프로젝션 대상} FROM
- 프로젝션 대상은 엔티티, 엠비디드 타입, 스칼라 타입이 있다.
엔티티 프로젝션
- 원하는 객체를 바로 조회
- 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
SELECT m FROM Member m // 회원
SELECT m.team FROM Member m // 팀
임베디드 타입 프로젝션
- 임베디드 타입은 엔티티와 거의 비슷하게 사용되지만, 조회의 시작점이 될 수 없다는 제약이 있다.
- 임베디드 타입은 엔티티 타입이 아닌 값 타입이기 때문에 영속성 컨텍스트에서 관리되지 않는다.
// 주소 조회
String query = "SELECT a From Address a"; // 조회의 시작점으로 사용할 수 없음. 잘못된 쿼리
String query = "SELECT o.address From Order o";
List<Address> resultList = em.createQuery(query, Address.class).getResultList();
스칼라 타입 프로젝션
- 스칼라 타입 = 숫자, 문자, 날짜와 같은 기본 데이터 타입
- 통계 쿼리도 주로 스칼라 타입으로 조회한다.
List<String> usernames =
em.createQuery("SELECT username FROM Member", Member.class)
.getResultList();
여러 값 조회
- 프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 Query를 사용해야 한다.
Query query = em.createQuery("SELECT m.username, m.age From Member m");
List resultList = query.getResultList();
- 여러 프로젝션을 처리하기 위해 제네릭에
Object[]
를 사용할 수 있지만, 실제 개발 시에는 UserDTO처럼 의미 있는 객체로 변환해서 사용한다.NEW
명령어를 사용하면 반환환받을 클래스를 지정할 수 있는데, 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있어 객체 변환을 간단하게 할 수 있다.
public class UserDTO {
private String username;
private int 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();
NEW
명령어 사용 시 주의할 점- 패키지 명을 포함한 전체 클래스 명을 입력해야한다.
- 순서와 타입이 일치하는 생성자가 필요하다.
4. 페이징 API
- JPA는 페이징을 다음 두 API로 추상화하였다.
setFirstResult(int startPosition)
: 조회 시작 위치(0부터 시작)setMaxResults(int maxResult)
: 조회할 데이터 수
query.setFirstResult(10); // 11번부터 시작
query.setMaxResults(20); // 11번 ~ 30번 데이터 조회
query.getResultList();
- 데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언(Dialect) 덕분이다.
- JPQL 은 방언에 따라 각기 다른 SQL로 변환된다
- ex. MySQL
LIMIT?,?
는 페이징을 위해 사용됨 - 첫 번째 물음표는 시작 인덱스, 두 번째 물음표는 반환할 레코드 수
"SELECT m FROM Member m ORDER BY m.username DESC" SELECT M.ID AS ID, M.AGE AS AGE. M.TEANM_ID AS TEAM_ID, M.NAME AS NAME FROM MEMBER M ORDER BY M.NAME DESC LIMIT?,?
5. 집합과 정렬
집합 함수
- COUNT, MAX, MIN, AVG, SUM
- NULL값은 무시하므로 통계에 잡히지 않는다.
- 만약 값이 없는데 집합 함수를 사용하면 NULL값이 된다. COUNT는 0이 된다.
- DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거한 후 집합을 구할 수 있다.
- DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.
GROUP BY, HAVING
- GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어 준다.
GROUP BY { 단일값 경로 | 별칭 }+
- HAVING은 GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링한다.
HAVING 조건식
- 그룹별 통계 데이터 중 나이가 10살 이상인 데이터
select t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
from Member m LEFT JOIN m.team t
GROUP BY t.name
HAVING AVG(m.age) >= 10
ORDER BY
- ASC : 오름차순(기본값)
- DESC : 내림차순
select t.name, COUNT(m.age) as cnt
from Member m LEFT JOIN m.team t
GROUP BY t.name
ORDER BY cnt
6. JPQL 조인
- SQL 조인과 기능은 같고 문법만 약간 다르다.
내부 조인
- 내부 조인은 INNER 조인을 사용하며, INNER은 생략할 수 있다.
- JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다.
FROM Member m INNER JOIN m.team t
회원이 가지고 있는 연관 필드(m.team)로 팀과 조인FROM Member m INNER JOIN Team t
SQL 조인처럼 사용하면 문법 오류 방생
- 연관 관계 필드는
m.team
처럼 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말한다.
SELECT m
FROM Member m INNER JOIN m.team t
where t.name = :teamName
외부 조인
- 외부 조인은 기능상 SQL의 외부 조인과 같다.
- OUTER는 생략 가능해서 보통 LEFT JOIN으로 사용한다.
SELECT m
FROM Member m LEFT JOIN m.team t
컬렉션 조인
- 일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 컬렉션 조인을 사용한다.
- [회원 → 팀] 조인은 다대일 조인이면서 단일 값 연관 필드(
m.team
)를 사용한다. - [팀 → 회원] 조인은 일대다 조인이면서 컬렉션 값 연관 필드(
t.members
)를 사용한다.- 팀과 팀이 보유한 회원 목록을 컬렉션 값 연관필드로 외부 조인
SELECT t, m FROM Team t LEFT JOIN t.members m
세타 조인
- WHERE절을 사용해서 세타 조인을 할 수 있다.
- 세타 조인은 내부 조인만 지원한다.
- 세타 조인을 사용하면 전혀 관계없는 엔티티도 조인할 수 있다.
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
JOIN ON절
- JPA 2.1부터 조인할 때 ON절을 지원한다.
- ON절을 사용하면 조인 대상을 필터링하고 조인할 수 있다.
- 내부 조인의 ON절은 WHERE절을 사용할 때와 같으므로 ON절은 보통 외부조인에서만 사용한다.
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'
7. 페치 조인
- 페치 조인은 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
fetch join
명령어로 연관된 엔티티나 컬렉션을 한 번에 같이 조회할 수 있다.[ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
엔티티 페치 조인
- 페치 조인을 사용하면 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회할 수 있다.
- JPQL 조인과는 다르게
m.team
다음에 별칭을 사용할 수 없다.- 하이버네이트는 페치 조인에도 별칭을 허용한다.
select m
from Member m join fetch m.team
- 회원 + 연관된 팀 함께 조회
- 만약에 회원과 팀을 지연로딩으로 설정했을 경우, 회원을 조회할 때 페치 조인을 사용해서 팀도 조회했으므로 지연로딩이 일어나지 않는다.
- 연관된 팀 엔티티가 프록시가 아닌 실제 엔티티이기 때문
- 따라서 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.
컬렉션 페치 조인
- 일대다 관계인 컬렉션 페치 조인
- 팀을 조회하면서 연관된 회원 컬렉션도 함께 조회
select t
from Team t join fetch t.members
where t.name = '팀A'
- TEAM 테이블에 팀 A는 하나지만 MEMBER 테이블과 조인하면서 결과가 증가해서 팀 A가 2건 조회
- 컬렉션 페치 조인 결과 객체에서 teams 결과 예제를 보면 주소가 같은 팀 A를 2건 가지게 된다.
- 일대다 조인은 결과가 증가할 수 있지만, 일대일이나 다대일 조인은 결과가 증가하지 않음
페치 조인과 DISTINCT
- SQL의 DISTINCT는 중복된 결과를 제거하는 명령어다.
- JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론이고 애플리케이션에서 한 번 더 중복을 제거한다.
- 컬렉션 페치 조인에서 팀 A가 중복으로 조회되는 문제
select distinct t
from Team t join fetch t.members
where t.name = '팀A'
- 그러나 각 로우의 데이터가 다르므로 SQL의 DISTINCT는 효과가 없다.
- 어플리케이션에서는 중복된 데이터를 걸러내어 중복인 팀 A가 하나만 조회
페치 조인과 일반 조인의 차이
- 일반조인
- JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT절에서 지정한 엔티티만 조회할 뿐이다.
- 지연 로딩을 설정하면 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환
- 즉시 로딩을 설정하면 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한 번 더 실행
- 페치 조인
- 지연 로딩이 설정되어 있어도 연관된 엔티티를 함께 조회
페치 조인의 특징
- 페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다.
- 최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하면 성능에 악영향을 미칠 수 있다. 따라서 글로벌 로딩 전략을 지연 로딩으로 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다.
- 준 영속 상태에서도 객체 그래프를 탐색할 수 있다.
페치 조인의 한계
- 페치 조인 대상에는 별칭을 줄 수 없다.
- 하이버네이트를 포함한 몇몇 구현체들은 페치 조인에 별칭을 지원한다. 하지만 잘못 사용하면 데이터 무결성이 깨질 수 있어 조심해야 한다.
- 둘 이상의 컬렉션을 페치할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
- 컬렉션(일대다)이 아닌 단일 값 연관 필드(일대일, 다대일)들은 가능하다.
8. 경로 표현식
• 경로 표현식은 .
을 찍어 객체 그래프를 탐색하는 것이다.
경로 표현식의 용어 정리
- 상태 필드: 단순히 값을 저장하기 위한 필드(필드 or 프로퍼티)
- 연관 필드: 연관 관계를 위한 필드 (임베디드 타입 포함)
- 단일 값 연관 필드: @ManyToOne, @OneToOne 대상 엔티티
- 컬렉션 값 연관 필드: @OneToMany, @ManyToMany 대상 컬렉션
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Colum(name="name")
private String username;
private Integer age;
@ManyToOne
private Team team;
@OneToMany
private List<Order> orders;
}
- 상태 필드 :
t.username
t.age
- 단일 값 연관 필드 :
m.team
- 컬렉션 값 연관 필드 :
m.orders
상태 필드 경로
- 경로 탐색의 끝이므로 더 탐색할 수 없다.
// JPQL select m.username, m.age from Member m // SQL select m.name, m.age from Member m
단일 값 연관 경로
- 묵시적으로 내부 조인이 일어나며, 계속 탐색할 수 있다.
- SQL에서 내부 조인이 일어난다. 이를 묵시적 조인이라고 하며, 묵시적 조인은 모두 내부 조인이므로 외부 조인을 사용하고 싶으면 명시적으로 JOIN 키워드를 사용해야 한다.
// JPQL select o.member from Order o // SQL select m.* from Orders o inner join Member m on o.member_id=m.id
컬렉션 값 연관 경로
- 묵시적으로 내부 조인이 일어나며, 더 탐색할 수 없다. 단 FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있다.
t.members.username
처럼 경로 탐색을 이어서 할 수 없다.select t.members from Team t join t.members m
과 같이 명시적 조인을 통해 새로운 별칭을 획득했다면 별칭을 통해 이어서 탐색이 가능하다.
select t.member from Team t
경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인이다.
- 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 한다.
- 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵기 때문에 되도록이면 명시적 조인을 사용하자
9. 서브 쿼리
JPQL에서 서브 쿼리는 WHERE, HAVING 절에만 사용할 수 있고 SELECT, HAVING절에는 사용할 수 없다.
EXISTS
[NOT] EXISTS (subquery)
- 서브쿼리에 결과가 존재하면 참 / NOT은 반대
{ALL | ANY | SOME}
{ALL | ANY | SOME} (subquery)
- 비교 연산자와 같이 사용
- ALL : 조건을 모두 만족하면 참
- ANY, SOME : 조건을 하나라도 만족하면 참
IN
[NOT] IN (subquery)
- 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
10. 조건식
BETWEEN
X [NOT] BETWEEN A AND B
- X는 A ~ B 사이의 값이면 참
LIKE
[NOT] LIKE 패턴값 [ESCAPE 이스케이프 문자]
- 문자 표현식과 패턴 값 비교
- % : 아무 값이 입력되어도 됨
- _ : 한 글자는 아무 값이 입력되어도 되지만 값이 있어야 한다.
NULL
{단일값 경로 | 입력 파라미터} IS [NOT] NULL
- NULL이지 비교
컬렉션식
{컬렉션 값 연관 경로} IS [NOT] NULL
{엔티티나 값} [NOT] MEMBER [OF]
CASE 식
CASE
{WHEN <조건식> THEN <스칼라식>}+
ELSE <스칼라식>
END
//예
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
11. 다형성 쿼리
JPQL로 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회된다.
TYPE
- 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정한다.
// Item중에 Book, Movie 조회
select i from Item i
where type(i) IN (Book, Movie)
TREAT
- 자바의 타입 캐스팅과 유사하여 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.
// 부모타입인 Item을 자식타입인 Book으로 다룸
select i from Item i
where treat(i as Book).author = 'kim'
12. 사용자 정의 함수 호출
- 사용할 데이터베이스 함수를 미리 persistence.xml에 등록하고, 아래 문법으로 사용하면 된다.
function_invocation::=FUNCTION(function_name {, function_arg}*)
//예
select fucntion('group_concat', i.name) from Item i
14. 엔티티 직접 사용
기본 키 값
- 객체 인스턴스는 참조 값으로 식별하고, 테이블 로우는 기본 키 값으로 식별한다.
- 따라서 JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 값을 사용한다.
// JPQL - 엔티티를 직접 사용
select count(m) from Member m
// SQL - 엔티티의 아이디를 사용
select count(m.id) as cnt from Member m
외래 키 값
Team team = em.find(Team.class, 1L);
String query = "select m from Member m where m.team = :team";
List resultList = em.createQuery(query)
.setParameter("team", team)
.getResultList();
- 기본 키 값이 1L인 팀 엔티티를 파라미터로 사용
- m.team은 team_id라는 외래키와 매핑
select m.*
from Member m
where m.team_id=? (팀 파라미터의 ID 값)
15. Named 쿼리: 정적 쿼리
- 동적 쿼리 :
em.createQuery()
처럼 JPQL을 문자로 완성해서 직접 넘기는 쿼리이다. 런타임에 따라 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다. - 정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할 때 재사용할 수 있고, 이를 Named 쿼리라 부른다. Named 쿼리는 한 번 정의하면 변경할 수 없다.
Named 쿼리
// 어노테이션에 정의
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username")
public class Member {
}
// 사용
List<Member> result List =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
'자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 12장 - 스프링 데이터 JPA (0) | 2023.07.26 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 10.4 - QueryDSL (0) | 2023.07.22 |
[자바 ORM 표준 JPA 프로그래밍] 9장 - 값 타입 (0) | 2023.07.22 |
[자바 ORM 표준 JPA 프로그래밍] 8장 - 프록시와 연관관계 관리 (0) | 2023.06.13 |
[자바 ORM 표준 JPA 프로그래밍] 7장 - 고급 매핑 (0) | 2023.06.12 |
자바 ORM 표준 JPA 프로그래밍 | 김영한 - 교보문고
자바 ORM 표준 JPA 프로그래밍 | 자바 ORM 표준 JPA는 SQL 작성 없이 객체를 데이터베이스에 직접 저장할 수 있게 도와주고, 객체와 관계형 데이터베이스의 차이도 중간에서 해결해준다. 이 책은 JPA
product.kyobobook.co.kr
1. 기본 문법과 쿼리 API
SELECT 문
SELECT m FROM Member AS m where m.username = 'Hello'
- 대소문자 구분
- 엔티티와 속성은 대소문자를 구분한다.
- Member와 username은 대소문자를 구분
- SELECT, FROM, AS 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
- 엔티티와 속성은 대소문자를 구분한다.
- 엔티티 이름
- JPQL에서 사용한 Member는 클래스명이 아니라 엔티티명이다.
@Entity(name="XXX")
로 엔티티명을 지정할 수 있다- 엔티티명을 지정하지 않으면 클래스명을 기본값으로 사용한다.
- 별칭은 필수
m
과 같은 별칭을 필수로 사용해야 하며, AS는 생략이 가능하다.
TypeQuery, Query
- 쿼리 객체
- 반환 타입을 명확하게 지정할 수 있으면 TypeQuery
- 반환 타입을 명확하게 지정할 수 없으면 Query
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = query.getResultList();
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList2 = query.getResultList();
for (Object o : resultList) {
Object[] result = (Object[]) o; // 결과가 둘 이상이면 Object[] 반환
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
}
- 조회대상이 회원이름(String)과 나이(Integer) 이므로 조회 타입이 명확하지 않아 Query 객체 사용
em.createQuery()
의 두 번째 파라미터에 반환할 타입을 지정하면 TypeQuery를 반환하고 지정하지 않으면 Query를 반환- 타입을 변환할 필요가 없는 TypeQuery를 사용하는 것을 추천
결과 조회
query.getResultList()
: 결과를 컬렉션으로 반환하며, 결과가 없으면 빈 컬렉션을 반환한다.query.getSingleResult()
: 결과가 정확히 하나일 때 사용하며, 객체 하나를 반환- 결과가 없거나, 1개보다 많으면 예외가 발생한다.
2. 파라미터 바인딩
이름 기준
- 파라미터 이름으로 구분하는 방법으로 이름 기준 파라미터는 앞에
:
를 사용한다.
String usernameParam = "User1";
TypeQuery<Member> query =
em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class);
.setParameter("username", usernameParam);
:username
이라는 파라미터 정의.setParameter()
에서 username이라는 이름으로 파라미터 바인딩
위치 기준
?
다음 에 위치값을 주면 되고, 위치 값은 1부터 시작한다.
List<member> resultList =
em.createQuery("SELECT m FROM Member m WHERE m.username = ?1", Member.class)
.setParameter(1, usernameParam)
.getResultList();
위치기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.
파라미터 바인딩 방식을 사용하지 않으면 SQL 인젝션 공격을 당할 수도 있다.
파라미터 바인딩 방식은 성능 향상에도 도움이 되므로 선택이 아닌 필수로 사용하자.
3. 프로젝션
- SELECT절에 조회할 대상을 지정하는 것을 프로젝션이라고 한다.
SELECT {프로젝션 대상} FROM
- 프로젝션 대상은 엔티티, 엠비디드 타입, 스칼라 타입이 있다.
엔티티 프로젝션
- 원하는 객체를 바로 조회
- 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
SELECT m FROM Member m // 회원
SELECT m.team FROM Member m // 팀
임베디드 타입 프로젝션
- 임베디드 타입은 엔티티와 거의 비슷하게 사용되지만, 조회의 시작점이 될 수 없다는 제약이 있다.
- 임베디드 타입은 엔티티 타입이 아닌 값 타입이기 때문에 영속성 컨텍스트에서 관리되지 않는다.
// 주소 조회
String query = "SELECT a From Address a"; // 조회의 시작점으로 사용할 수 없음. 잘못된 쿼리
String query = "SELECT o.address From Order o";
List<Address> resultList = em.createQuery(query, Address.class).getResultList();
스칼라 타입 프로젝션
- 스칼라 타입 = 숫자, 문자, 날짜와 같은 기본 데이터 타입
- 통계 쿼리도 주로 스칼라 타입으로 조회한다.
List<String> usernames =
em.createQuery("SELECT username FROM Member", Member.class)
.getResultList();
여러 값 조회
- 프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 Query를 사용해야 한다.
Query query = em.createQuery("SELECT m.username, m.age From Member m");
List resultList = query.getResultList();
- 여러 프로젝션을 처리하기 위해 제네릭에
Object[]
를 사용할 수 있지만, 실제 개발 시에는 UserDTO처럼 의미 있는 객체로 변환해서 사용한다.NEW
명령어를 사용하면 반환환받을 클래스를 지정할 수 있는데, 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있어 객체 변환을 간단하게 할 수 있다.
public class UserDTO {
private String username;
private int 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();
NEW
명령어 사용 시 주의할 점- 패키지 명을 포함한 전체 클래스 명을 입력해야한다.
- 순서와 타입이 일치하는 생성자가 필요하다.
4. 페이징 API
- JPA는 페이징을 다음 두 API로 추상화하였다.
setFirstResult(int startPosition)
: 조회 시작 위치(0부터 시작)setMaxResults(int maxResult)
: 조회할 데이터 수
query.setFirstResult(10); // 11번부터 시작
query.setMaxResults(20); // 11번 ~ 30번 데이터 조회
query.getResultList();
- 데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언(Dialect) 덕분이다.
- JPQL 은 방언에 따라 각기 다른 SQL로 변환된다
- ex. MySQL
LIMIT?,?
는 페이징을 위해 사용됨 - 첫 번째 물음표는 시작 인덱스, 두 번째 물음표는 반환할 레코드 수
"SELECT m FROM Member m ORDER BY m.username DESC" SELECT M.ID AS ID, M.AGE AS AGE. M.TEANM_ID AS TEAM_ID, M.NAME AS NAME FROM MEMBER M ORDER BY M.NAME DESC LIMIT?,?
5. 집합과 정렬
집합 함수
- COUNT, MAX, MIN, AVG, SUM
- NULL값은 무시하므로 통계에 잡히지 않는다.
- 만약 값이 없는데 집합 함수를 사용하면 NULL값이 된다. COUNT는 0이 된다.
- DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거한 후 집합을 구할 수 있다.
- DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.
GROUP BY, HAVING
- GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어 준다.
GROUP BY { 단일값 경로 | 별칭 }+
- HAVING은 GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링한다.
HAVING 조건식
- 그룹별 통계 데이터 중 나이가 10살 이상인 데이터
select t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
from Member m LEFT JOIN m.team t
GROUP BY t.name
HAVING AVG(m.age) >= 10
ORDER BY
- ASC : 오름차순(기본값)
- DESC : 내림차순
select t.name, COUNT(m.age) as cnt
from Member m LEFT JOIN m.team t
GROUP BY t.name
ORDER BY cnt
6. JPQL 조인
- SQL 조인과 기능은 같고 문법만 약간 다르다.
내부 조인
- 내부 조인은 INNER 조인을 사용하며, INNER은 생략할 수 있다.
- JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다.
FROM Member m INNER JOIN m.team t
회원이 가지고 있는 연관 필드(m.team)로 팀과 조인FROM Member m INNER JOIN Team t
SQL 조인처럼 사용하면 문법 오류 방생
- 연관 관계 필드는
m.team
처럼 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말한다.
SELECT m
FROM Member m INNER JOIN m.team t
where t.name = :teamName
외부 조인
- 외부 조인은 기능상 SQL의 외부 조인과 같다.
- OUTER는 생략 가능해서 보통 LEFT JOIN으로 사용한다.
SELECT m
FROM Member m LEFT JOIN m.team t
컬렉션 조인
- 일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 컬렉션 조인을 사용한다.
- [회원 → 팀] 조인은 다대일 조인이면서 단일 값 연관 필드(
m.team
)를 사용한다. - [팀 → 회원] 조인은 일대다 조인이면서 컬렉션 값 연관 필드(
t.members
)를 사용한다.- 팀과 팀이 보유한 회원 목록을 컬렉션 값 연관필드로 외부 조인
SELECT t, m FROM Team t LEFT JOIN t.members m
세타 조인
- WHERE절을 사용해서 세타 조인을 할 수 있다.
- 세타 조인은 내부 조인만 지원한다.
- 세타 조인을 사용하면 전혀 관계없는 엔티티도 조인할 수 있다.
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
JOIN ON절
- JPA 2.1부터 조인할 때 ON절을 지원한다.
- ON절을 사용하면 조인 대상을 필터링하고 조인할 수 있다.
- 내부 조인의 ON절은 WHERE절을 사용할 때와 같으므로 ON절은 보통 외부조인에서만 사용한다.
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'
7. 페치 조인
- 페치 조인은 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
fetch join
명령어로 연관된 엔티티나 컬렉션을 한 번에 같이 조회할 수 있다.[ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
엔티티 페치 조인
- 페치 조인을 사용하면 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회할 수 있다.
- JPQL 조인과는 다르게
m.team
다음에 별칭을 사용할 수 없다.- 하이버네이트는 페치 조인에도 별칭을 허용한다.
select m
from Member m join fetch m.team
- 회원 + 연관된 팀 함께 조회
- 만약에 회원과 팀을 지연로딩으로 설정했을 경우, 회원을 조회할 때 페치 조인을 사용해서 팀도 조회했으므로 지연로딩이 일어나지 않는다.
- 연관된 팀 엔티티가 프록시가 아닌 실제 엔티티이기 때문
- 따라서 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.
컬렉션 페치 조인
- 일대다 관계인 컬렉션 페치 조인
- 팀을 조회하면서 연관된 회원 컬렉션도 함께 조회
select t
from Team t join fetch t.members
where t.name = '팀A'
- TEAM 테이블에 팀 A는 하나지만 MEMBER 테이블과 조인하면서 결과가 증가해서 팀 A가 2건 조회
- 컬렉션 페치 조인 결과 객체에서 teams 결과 예제를 보면 주소가 같은 팀 A를 2건 가지게 된다.
- 일대다 조인은 결과가 증가할 수 있지만, 일대일이나 다대일 조인은 결과가 증가하지 않음
페치 조인과 DISTINCT
- SQL의 DISTINCT는 중복된 결과를 제거하는 명령어다.
- JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론이고 애플리케이션에서 한 번 더 중복을 제거한다.
- 컬렉션 페치 조인에서 팀 A가 중복으로 조회되는 문제
select distinct t
from Team t join fetch t.members
where t.name = '팀A'
- 그러나 각 로우의 데이터가 다르므로 SQL의 DISTINCT는 효과가 없다.
- 어플리케이션에서는 중복된 데이터를 걸러내어 중복인 팀 A가 하나만 조회
페치 조인과 일반 조인의 차이
- 일반조인
- JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT절에서 지정한 엔티티만 조회할 뿐이다.
- 지연 로딩을 설정하면 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환
- 즉시 로딩을 설정하면 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한 번 더 실행
- 페치 조인
- 지연 로딩이 설정되어 있어도 연관된 엔티티를 함께 조회
페치 조인의 특징
- 페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다.
- 최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하면 성능에 악영향을 미칠 수 있다. 따라서 글로벌 로딩 전략을 지연 로딩으로 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다.
- 준 영속 상태에서도 객체 그래프를 탐색할 수 있다.
페치 조인의 한계
- 페치 조인 대상에는 별칭을 줄 수 없다.
- 하이버네이트를 포함한 몇몇 구현체들은 페치 조인에 별칭을 지원한다. 하지만 잘못 사용하면 데이터 무결성이 깨질 수 있어 조심해야 한다.
- 둘 이상의 컬렉션을 페치할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
- 컬렉션(일대다)이 아닌 단일 값 연관 필드(일대일, 다대일)들은 가능하다.
8. 경로 표현식
• 경로 표현식은 .
을 찍어 객체 그래프를 탐색하는 것이다.
경로 표현식의 용어 정리
- 상태 필드: 단순히 값을 저장하기 위한 필드(필드 or 프로퍼티)
- 연관 필드: 연관 관계를 위한 필드 (임베디드 타입 포함)
- 단일 값 연관 필드: @ManyToOne, @OneToOne 대상 엔티티
- 컬렉션 값 연관 필드: @OneToMany, @ManyToMany 대상 컬렉션
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Colum(name="name")
private String username;
private Integer age;
@ManyToOne
private Team team;
@OneToMany
private List<Order> orders;
}
- 상태 필드 :
t.username
t.age
- 단일 값 연관 필드 :
m.team
- 컬렉션 값 연관 필드 :
m.orders
상태 필드 경로
- 경로 탐색의 끝이므로 더 탐색할 수 없다.
// JPQL select m.username, m.age from Member m // SQL select m.name, m.age from Member m
단일 값 연관 경로
- 묵시적으로 내부 조인이 일어나며, 계속 탐색할 수 있다.
- SQL에서 내부 조인이 일어난다. 이를 묵시적 조인이라고 하며, 묵시적 조인은 모두 내부 조인이므로 외부 조인을 사용하고 싶으면 명시적으로 JOIN 키워드를 사용해야 한다.
// JPQL select o.member from Order o // SQL select m.* from Orders o inner join Member m on o.member_id=m.id
컬렉션 값 연관 경로
- 묵시적으로 내부 조인이 일어나며, 더 탐색할 수 없다. 단 FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있다.
t.members.username
처럼 경로 탐색을 이어서 할 수 없다.select t.members from Team t join t.members m
과 같이 명시적 조인을 통해 새로운 별칭을 획득했다면 별칭을 통해 이어서 탐색이 가능하다.
select t.member from Team t
경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인이다.
- 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 한다.
- 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵기 때문에 되도록이면 명시적 조인을 사용하자
9. 서브 쿼리
JPQL에서 서브 쿼리는 WHERE, HAVING 절에만 사용할 수 있고 SELECT, HAVING절에는 사용할 수 없다.
EXISTS
[NOT] EXISTS (subquery)
- 서브쿼리에 결과가 존재하면 참 / NOT은 반대
{ALL | ANY | SOME}
{ALL | ANY | SOME} (subquery)
- 비교 연산자와 같이 사용
- ALL : 조건을 모두 만족하면 참
- ANY, SOME : 조건을 하나라도 만족하면 참
IN
[NOT] IN (subquery)
- 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
10. 조건식
BETWEEN
X [NOT] BETWEEN A AND B
- X는 A ~ B 사이의 값이면 참
LIKE
[NOT] LIKE 패턴값 [ESCAPE 이스케이프 문자]
- 문자 표현식과 패턴 값 비교
- % : 아무 값이 입력되어도 됨
- _ : 한 글자는 아무 값이 입력되어도 되지만 값이 있어야 한다.
NULL
{단일값 경로 | 입력 파라미터} IS [NOT] NULL
- NULL이지 비교
컬렉션식
{컬렉션 값 연관 경로} IS [NOT] NULL
{엔티티나 값} [NOT] MEMBER [OF]
CASE 식
CASE
{WHEN <조건식> THEN <스칼라식>}+
ELSE <스칼라식>
END
//예
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
11. 다형성 쿼리
JPQL로 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회된다.
TYPE
- 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정한다.
// Item중에 Book, Movie 조회
select i from Item i
where type(i) IN (Book, Movie)
TREAT
- 자바의 타입 캐스팅과 유사하여 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.
// 부모타입인 Item을 자식타입인 Book으로 다룸
select i from Item i
where treat(i as Book).author = 'kim'
12. 사용자 정의 함수 호출
- 사용할 데이터베이스 함수를 미리 persistence.xml에 등록하고, 아래 문법으로 사용하면 된다.
function_invocation::=FUNCTION(function_name {, function_arg}*)
//예
select fucntion('group_concat', i.name) from Item i
14. 엔티티 직접 사용
기본 키 값
- 객체 인스턴스는 참조 값으로 식별하고, 테이블 로우는 기본 키 값으로 식별한다.
- 따라서 JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 값을 사용한다.
// JPQL - 엔티티를 직접 사용
select count(m) from Member m
// SQL - 엔티티의 아이디를 사용
select count(m.id) as cnt from Member m
외래 키 값
Team team = em.find(Team.class, 1L);
String query = "select m from Member m where m.team = :team";
List resultList = em.createQuery(query)
.setParameter("team", team)
.getResultList();
- 기본 키 값이 1L인 팀 엔티티를 파라미터로 사용
- m.team은 team_id라는 외래키와 매핑
select m.*
from Member m
where m.team_id=? (팀 파라미터의 ID 값)
15. Named 쿼리: 정적 쿼리
- 동적 쿼리 :
em.createQuery()
처럼 JPQL을 문자로 완성해서 직접 넘기는 쿼리이다. 런타임에 따라 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다. - 정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할 때 재사용할 수 있고, 이를 Named 쿼리라 부른다. Named 쿼리는 한 번 정의하면 변경할 수 없다.
Named 쿼리
// 어노테이션에 정의
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username")
public class Member {
}
// 사용
List<Member> result List =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
'자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 12장 - 스프링 데이터 JPA (0) | 2023.07.26 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 10.4 - QueryDSL (0) | 2023.07.22 |
[자바 ORM 표준 JPA 프로그래밍] 9장 - 값 타입 (0) | 2023.07.22 |
[자바 ORM 표준 JPA 프로그래밍] 8장 - 프록시와 연관관계 관리 (0) | 2023.06.13 |
[자바 ORM 표준 JPA 프로그래밍] 7장 - 고급 매핑 (0) | 2023.06.12 |