Reference. 자바 ORM 표준 JPA 프로그래밍
책 목차 및 이전 글
더보기
들어가기 전 JPA 특징, Q&A
1. JPA 소개
3. 영속성 관리
4. 엔티티 매핑
4.1 - 4.3 @Entity, @Table, 다양한 매핑
4.4 - 4.5 데이터베이스 스키마 자동 생성, DDL 생성 기능
5. 연관관계 매핑 기초
6. 다양한 연관관계 매핑
7. 고급매핑
8. 프록시와 연관관계 관리
9. 값 타입
9.3~5 값 타입과 불변 객체, 값 비교, 값 타입 컬렉션
10. 객체지향 쿼리 언어
10.2 JPQL
JPQL의 특징
- 객체지향 쿼리 언어, 데이터베이스의 테이블을 대상으로 쿼리하는 것이 아닌 객체 대상으로 쿼리
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존적이지 않음
- 마지막엔 SQL로 변환
10.2.1 기본 문법과 쿼리 API
- JPQL도 SELECT, UPDATE, DELETE문 사용 가능
- 엔티티를 저장할때는
EntityManager.persist()
메소드를 사용하므로 INSERT문은 없음
JPQL 문법
select_문 :: = select_절 from_절 [where_절] [groupby_절] [having_절] [orderby_절] update_문 :: = update_절 [where_절] delete_문 :: = delete_절 [where_절]
- 엔티티를 저장할때는
SELECT 문
SELECT 기본 사용
SELECT m FROM Member AS m where m.username = 'Hello'
대소문자 구분
- 엔티티와 속성은 대소문자를 구분 (Member, username) JPQL 키워드는 대소문자 구분하지 않음 (SELECT, FROM, AS)
엔티티 이름
- Member는 클래스 명이 아니라 엔티티명 (
@Entity(name="xxx")
로 지정 가능 )
- 지정하지 않으면 클래스명이 기본값, 클래스 명을 엔티티로 사용하는 것을 추천
별칭은 필수
- JPQL은 Member AS m과 같은 별칭은 필수, 별칭이 없는 경우 오류 발생
SELECT username FROM member m //잘못된 문법, username -> m.username으로 수정 필요)
- 하이버네이트를 사용하면 HQL도 사용 가능 HQL은 별칭 없이 사용이 가능 (JPQL X)
TypeQuery, Query
- 쿼리 객체는 TypeQuery와 Query가 존재
- 반환할 타입을 명확하게 지정이 가능하면
TypeQuery
, 명확하지 않으면Query
사용
TypeQuery 사용
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class); List<Member> resultList = query.getResultList(); for (Member member : resultList) { System.out.println("member = " + member); }
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; //결과가 둘 이상이라 배열 System.out.println("username = " + result[0]); System.out.println("age = " + result[1]); }
- 타입을 변환할 필요가 없는 TypeQuery를 사용하는 것이 더 편리
- 반환할 타입을 명확하게 지정이 가능하면
결과 조회
실제 쿼리를 실행해서 데이터베이스를 조회하는 메소드 2가지
query.getResultList()
: 결과를 List 컬렉션으로 반환, 만약 결과가 없으면 빈 컬렉션을 반환
query.getSingleResult()
: 결과가 정확히 하나일 때 사용- 결과가 없으면
javax.persistence.NoResultException
예외 발생
- 결과가 1개보다 많으면
javax.persistence.NonUniqueResultException
예외 발생
- 결과가 없으면
10.2.2 파라미터 바인딩
- JPQL은
이름 기준
,위치 기준
파라미터 바인딩을 지원
- 이름 기준 파라미터
- 이름으로 구분하는 방법으로 이름 기준 파라미터는 앞에 :를 사용
String usernameParam = "User1"; //기본 방법 TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class); query.setParameter("username", usernameParam); List<Member> resultList = query.getResultList(); //체인 방식 방법 List<Member> members = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class) .setParameter("username", usernameParam) .getResultList();
- 위치 기준 파라미터
- 위치 기준파라미터를 사용하려면 ? 다음에 위치 값을 지정
//체인 방식 방법 List<Member> members = em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class) .setParameter(1, usernameParam) .getResultList();
- 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확
- 파라미터 바인딩 방식을 사용하지 않으면 SQL 인젝션 공격과 성능이슈가 발생
JPA는 JPQL을 SQL의 파싱결과를 재사용하여 성능 향상, 파라미터 바인딩은 선택이 아닌 필수
//위험한 코드 (SQL인젝션 공격과 성능이슈 발생) "select m from Member m where m.username = '" + usernameParam + "'"
10.2.3 프로젝션
- SELECT 절에 조회할 대상을 지정하는 것을 프로젝션(projection)
- 프로젝션 대상은 엔티티, 엠비디드 타입, 스칼라 타입[숫자, 문자 등 기본 데이터 타입을 의미]
엔티티 프로젝션
- 엔티티를 프로젝션 대상으로 사용, 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리
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> addresses = em.createQuery(query, Address.class).getResultList();
- 임베디드 타입은 엔티티타입이 아닌 값 타입, 임베디드 타입은 영속성 컨텍스트에서 관리X
스칼라 타입 프로젝션
- 숫자, 문자, 날짜와 같은 기본 데이터 타입들이 스칼라 타입
List<String> usernames = em.createQuery("SELECT username FROM Member m", String.class) .getResultList(); Double orderAmountAvg = em.createQuery("SELECT AVG(o.orderAmount) FROM Order o", Double.class) .getSingleResult()
여러 값 조회
- 엔티티 대상이 아닌 꼭 필요한 데이터들만 선택해서 조회할 때 사용
List<Object[]> resultList = em.createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o") .getResultList(); for (Object[] row : resultList) { Member member = (Member) row[0]; //엔티티 Product product = (Product) row[1]; //엔티티 int orderAmount = (Integer) row[2]; //스칼라 }
- 조회한 엔티티는 영속성 컨텍스트에서 관리
NEW 명령어
- 실무에서는 Object[]를 직접 사용하지 않고 DTO를 만들어 객체로 반환해서 사용
public class UserDTO { private String username; private int age; public UserDTO(String username, int age) { this.username = username; this.age = age; } ... }
//-------------1. DTO를 사용해서 컬렉션에 저장 후 리턴------------- List<Object[]> resultList = em.createQuery("SELECT m.username, m.age FROM Member m") .getResultList(); List<UserDTO> userDTOs = new ArrayList<UserDTO>(); for (Object[] row : resultList) { UserDTO userDTO = new UserDTO((String)row[0], (Integer)row[1]); userDTOs.add(userDTO); } return userDTOs; //-------------2. new 명령어를 사용하여 저장 후 리턴------------- TypeQuery<UserDTO> query = em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m"); List<UserDTO> resultList = query.getResultList(); return resultList;
- 1번에서 new 명령어를 통해 변환하므로 지루한 변환작업을 하지 않아도 동일 결과가 리턴
- NEW 명령어를 사용할 떄 주의해야 할 2가지
- 패키지 명을 포함한 전체 클레스 명을 입력 (
jpabook.jpql.UserDTO
)
- 순서와 타입이 일치하는 생성자가 필요(
UserDTO(m.username, m.age)
)
- 패키지 명을 포함한 전체 클레스 명을 입력 (
10.2.4 페이징 API
- 페이징 처리용 SQL은 지루하고 반복적인 일이며, 데이터베이스마다 페이징 처리 문법이 다름
- JPA는 페이징을 두 API로 추상화 처리
setFirstResult(int startPosition)
: 조회 시작 위치(0부터 시작)
setMaxResults(int maxResult)
: 조회할 데이터 수
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class); query.setFirstResult(10); query.setMaxResults(20); query.getResultList();
FirstResult
시작이 10이므로 11부터 시작해서 20건을 조회 (11~30번 데이터 조회)
- 데이터 베이스 방언(Dialect) 덕분에 같은 API로 처리 가능
JPA(Dialect) 페이징 API 데이터베이스별 쿼리
HSQLDB
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M
ORDER BY
M.NAME DESC OFFSET ? LIMIT ?
MySQL
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M
ORDER BY
M.NAME DESC LIMIT ?, ?
PostgreSQL
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M
ORDER BY
M.NAME DESC LIMIT ? OFFSET ?
오라클
SELECT *
FROM
( SELECT ROW_.*, ROWNUM ROWNUM_
FROM
( SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM MEMBER M
ORDER BY M.NAME
) ROW_
WHERE ROWNUM <= ?
)
WHERE ROWNUM_ > ?
SQL Server
WITH query AS (
SELECT
inner_query.*,
ROW_NUMBER() OVER (ORDER BY CURRNT_TIMESTAMP) as __hibernate_row_nr__
FROM
( select
TOP(?) m.id as id,
m.age as age,
m.team as team_id,
m.name as name,
from Member m
order by m.name DESC
) inner_query
)
SELECT id, age, team_id, name
FROM query
where __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?
10.2.5 집합과 정렬
- 집합은 집합함수와 함께 통계 정보를 구할 때 주로 사용
select COUNT(m), //회원수 SUM(m.age), //나이 합 AVG(m.age), //평균 나이 MAX(m.age), //최대 나이 MIN(m.age) //최소 나이 from Member m
집함 함수
함수 | 설명 |
---|---|
COUNT | 결과 수를 반환. (Long) |
MAX, MIN | 최대, 최소 값을 반환. 문자, 숫자 날짜 등에 사용 |
AVG | 평균값을 반환. 숫자타입만 사용 가능 (Dobule) |
SUM | 합을 반환. 숫자타입만 사용 가능 (정수형: Long, 소수합: Double, BigInter, BigDecimal) |
집함 함수 사용시 참고사항
- NULL값은 무시, 통계에 값이 있는 것만 결과에 반영 ( NULL은 무시)
- 만약 값이 없는데 집합 함수(SUM, AVG 등)를 사용하면 NULL 값 (단, COUNT는 0)
- DISTINCT를 집합 함수 안에 사용해서 중복 제거 후 집합 함수 사용 가능
select COUNT( DISTINCT m.age ) from Member m
- DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않음
GROUP BY, HAVING
- GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어주는 역할
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은 GROUP BY와 함께 사용하며, 그룹화한 통계 데이터를 기준으로 필터링 (WHERE 역할)
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)
- 결과를 정렬할 때 사용
- orderby_절 :: = ORDER BY {상태필드 경로 | 결과 변수 {ASC | DESC}}+
- ASC : 오름차순 (기본값)
- DESC: 내림차순
10.2.6 JPQL 조인
- JPQL도 SQL 조인과 기능은 같고 문법만 약간 다른 조인을 지원
내부 조인
- INNER JOIN을 사용, INNER는 생략 가능
//JPQL SELECT m FROM Member m INNER JOIN m.team t where t.name = '팀A'; //SQL SELECT M.ID AS ID, M.AGE AS AGE, M.TEAM_ID AS TEAM_ID, M.NAME AS NAME FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID WHERE T.NAME = ?
Member m JOIN m.team t
: 연관 필드로 팀과 조인. 조인한 팀에는 t라는 별칭이 필요FROM Member m JOIN Team t //잘못된 JPQL 조인, 오류 발생
- 서로 다른 타입의 두 엔티티를 조회하면
TypeQuery
못 씀. 다음처럼 조회해야 가능String query = "SELECT m, t FROM Member m JOIN m.team t"; List<Object[]> result = em.createQuery(query).getResultList(); for (Object[] row : result) { Member member = (Member) row[0]; Team team = (Team) row[1]; }
외부 조인
- LEFT [OUTER] JOIN을 사용, OUTER는 보통 생략
//JPQL 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 OUTER JOIN TEAM T ON M.TEAM_ID = T.ID WHERE T.NAME = ?
컬렉션 조인
- 일대다, 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 의미
- [회원 → 팀]의 조인은 다대일 조인이면서 단일 값 연관 필드(m.team)를 사용
- [팀 → 회원]의 조인은 일대다 조인이면서 컬렉션 값 연관 필드(m.members)를 사용
SELECT t, m From Team t LEFT JOIN t.members m
- 컬렉션 값 연관 필드는 외부 조인을 사용 (내부 조인시 조회 안되는 문제 발생)
세타 조인
- WHERE 절을 사용하며, 세타 조인은 내부 조인만 지원
- 세타 조인을 사용하면 전혀 관계 없는 엔티티도 조인이 가능
//JPQL select coumt(m) from Member m, Team t where m.username = t.name //SQL SELECT COUNT(M.ID) FROM MEBER M CROSS JOIN TEAM T WHERE M.USERNAME = T.NAME
JOIN ON 절(JPA 2.1)
- JPA 2.1부터 조인할 때 ON 절을 지원
- ON 절을 사용하면 조인 대상을 피터링 한 후 조인이 가능
//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'
10.2.7 페치 조인
- SQL에서 이야기하는 조인 종류가 아닌 JPQ 성능 최적화를 위해 제공하는 기능
엔티티 페치 조인
- 연관된 엔티티나 컬렉션을 함께 조회, JPQL 조인과는 다르게 페치 조인은 별칭 사용 못함
(하이버네이트는 페치 조인에도 별칭 허용)
//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
조회했지만 연관된 팀도 함께 조회 (fetch 조인은 연관 엔티티 조회)
- JPQL에서
컬렉션 페치 조인
//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'
- 팀A는 하나지만 Member 테이블과 조인하면서 결과 값 증가 (팀 A 2건 조회)
※ 일대다 조인은 결과가 증가할 수 있지만 일대일 ,다대일은 결과가 증가하지 않음
컬렉션 페치 조인 사용시 나오는 문제
- 동일한 결과값이 2번 출력되는 현상 발생 (팀 A가 2건 조회)
String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"
List<Team> team = em.createQuery(jpql, Team.class).getResultList();
for(Team team: teams) {
System.out.println("teamname = " + team.getName() + ", team = " + team);
for (Member member : team.getMembers()) {
System.out.println("->username = " + member.getUsername() +
", member = " + member);
}
}
//출력 결과
teamname = 팀A, team = Team@0x100
->username = 회원1, member = Member@0x200
->username = 회원2, member = Member@0x200
teamname = 팀A, team = Team@0x100
->username = 회원1, member = Member@0x200
->username = 회원2, member = Member@0x200
페치 조인과 DISTINCT
- JPQL의 DISTINCT명령어는 SQL에 DISTINCT를 추가하고, 애플리케이션에서 한번 더 중복을 제거
//JPQL select distinct t from Team t join fetch t.members where t.name = '팀A' //애플리케이션 출력 결과 teamname = 팀A, team = Team@0x100 ->username = 회원1, member = Member@0x200 ->username = 회원2, member = Member@0x200
- SQL의 DISTINCT를 추가하여도 데이터가 다르므로 효과가 없음
- 애플리케이션에서 중복된 데이터를 제거하므로 하나만 조회
페치 조인과 일반 조인의 차이
- JPQL은 결과를 반환할 때 연관관계는 고려하지 않고 단지 SELECT 절에 엔티티만을 조회
//내부 조인 JPQL select t from Team t join t.member m where t.name = '팀A' //실행된 SQL SELECT T.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A' //페치 조인의 경우 SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
- 회원 컬렉션을 지연 로딩으로 설정하면 프록시나 초기화하지 않은 컬렉션 래퍼를 반환 즉시 로딩으로 설정하면 회원 컬렉션을 위해 쿼리를 한번 더 실행
페치 조인의 특징과 한계
글로벌 로딩 전략
: 엔티티에 직접 적용하는 로딩 전략을 의미, 애플리케이션 전체 영향을 발생@OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
- 여러 테이블을 조인해 엔티티가 아닌 전혀 다른 결과가 필요한 경우 DTO를 쓰는게 효과적
페치 조인의 장점
- 페치 조인은 연관된 엔티티를 함께 조회하여 SQL 호출 횟수를 줄여 성능 최적화가 가능
- 페치 조인은 글로벌 로딩 전략보다 우선 적용 ( 우선순위: 페치 조인 > 글로벌 로딩전략 )
- 글로벌 로딩을 즉시 로딩을 쓰면 성능에 악영향인 경우(자주 로딩) 변경에 어려움 발생 글로벌 로딩을 지연 로딩으로 사용하고 최적화가 필요하면 페치 조인을 사용이 효과적
- 연관된 엔티티를 쿼리 시점에 조회하므로 지연로딩 발생하지 않음 준영속 상태에서도 객체 그래프 탐색 가능
페치 조인의 단점
페치 조인 대상에 별칭을 쓸 수 없음
- 별칭이 없으므로 SELECT, WHERE, 서브 쿼리에 페치 조인 대상을 사용 못함
- JPA표준에는 지원하지 않지만 하이버네이트 등 몇몇 구현체는 별칭을 지원 하지만 별칭을 잘못사용하면 데이터 무결성에 문제가 발생 (데이터가 수가 틀림) 2차 캐시와 함께 쓸 때 데이터 수가 달라질 경우 다른 곳에 연관된 데이터도 문제 발생
둘 이상의 컬렉션을 페치 못함
- 구현체에 따라 될수도 있지만 ( 컬렉션 * 컬렉션 )의 카테시안 곱이 발생하여 주의 필요
- 하이버네이트에선
org.hibernate.loader.MultipleBagFetchException
예외 발생
컬렉션을 페치 조인하면 페이징 API(setFirstResult
, setMaxResults
) 사용 못함
- 컬렉션(일대다)이 아닌 단일 필드(일대일, 다대일)들은 페이징 API 사용 가능
- 하이버네이트에선 컬렉션을 페치 조인하고 페이징 API사용하면 경고 로그 발생 후 처리
- 데이터가 적으면 상관 없지만 많은 경우 성능 이슈와 메모리 초과 예외 발생하므로 위험
10.2.8 경로 표현식
- 경로 표현식이라는 것은 쉽게 이야기해서 .(점)을 찍어 객체 그래프를 탐색하는 것을 의미
경로 표현식의 용어 정리
- 상태 필드: 단순히 값을 저장하기 위한 필드 (
m.username
,m.age
)
- 연관 필드: 연관관계를 위한 필드, 임베디드 타입 포함
- 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티 (
m.team
)
- 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션 (
t.members
)
- 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티 (
명시적 조인과 묵시적 조인
- 명시적 조인: JOIN을 직접 적어주는 것
SELECT m FROM Member m JOIN m.team t
- 묵시적 조인: 경로 표현식에 의해 묵시적으로 조인이 발생하는 것, 내부조인만 가능
SELECT m.team FROM Member m
경로 표현식과 특징
상태 필드 경로
: 경로 탐색의 끝, 더는 탐색 못함- 상태 필드 경로 탐색
//JPQL select m.username, m.age from Member
- 상태 필드 경로 탐색
단일 값 연관 경로
: 묵시적으로 내부 조인 발생, 값 연관 경로 계속 탐색 가능 (외부 조인은 명시적으로 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 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색 가능- 컬렉션 값 연관 경로 탐색
- JPQL에서 많이 하는 실수가 컬렉션 값 연관 경로 탐색을 시도하는 것
select t.members from Team t //성공 select t.members.username from Team t//실패 -> select m.username from Team t join t.members m //새로운 별칭 필요
- 컬렉션은 컬렉션의 크기를 구하는 size라는 기능 사용 가능 (SQL COUNT 함수 사용)
select t.members.size from Team t //성공
- 컬렉션 값 연관 경로 탐색
경로 표현식의 복잡한 예시
- 임베디드 타입 접근도 단일 값 경로 탐색이지만 주문 테이블에 포함되어 조인 발생X
//JPQL
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='JINJU'
경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인 (INNER JOIN)
- 컬렉션은 경로 탐색의 끝, 컬렉션에서 경로 탐색을 하려면 명시적인 조인 별칭이 필요
- 경로 탐색은 주로 SELECT, WHERE절에서 사용하지만 묵시적 조인으로 인해 FROM절에 영향
- 묵시적 조인은 조인이 일어나는 상황을 파악하기 어렵다는 단점, 명시적 조인 사용을 추천
10.2.9 서브쿼리
- JPQL도 서브쿼리를 지원
- WHERE, HAVING 절에서만 사용 가능, SELECT, FROM 절에는 사용 불가
- 하이버네이트 HQL은 SELECT 절의 서브 쿼리 허용
select m from Member m where m.age > (select avg(m2.age) from Member m2)
서브 쿼리 함수
[NOT] EXISTS (subquery)
: 서브쿼리에 결과가 존재하면 참. NOT은 반대//예: 팀 A소속인 회원 select m from Member m where exists (select t from m.team t where t.name = '팀A')
{ALL | ANY | SOME} (subquery)
: 비교 연산자와 같이 사용- ALL: 조건을 모두 만족하면 참
- ANY 혹은 SOME : 둘다 같은 의미이며 조건을 하나라도 만족하면 참
//예: 전체 상품 각각의 재고보다 주문량이 많은 주문들 select o from 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)
[NOT] IN (subquery)
: 서브쿼리 결과가 하나라도 같은게 있으면 참. 서브쿼리 아닌 곳도 사용//예: 20세 이상을 보유한 팀 select t from Team t where t IN (select t2 From Team t2 JOIN t2.members m2 where m2.age >= 20)
10.2.10 조건식
타입 표현
종류 | 설명 | 예제 |
---|---|---|
문자 | 작은 따옴표 사이에 표현 작은 따옴표를 표현하고 싶으면 작은 따옴표 연속 주개('') 사용 | 'HELLO' 'She''s' |
숫자 | L (Long 타입 지정) D (Double 타입 지정) F (Float 타입 지정) | 10L 10D 10F |
날짜 | DATE {d 'yyyy-mm-dd'} TIME (t 'hh-mm-ss'} DATETIME {ts 'yyyy-mm-dd hh:mm:ss.f'} | {d '2021-08-21'} {t '10-11-11'} {ts '2021-08-21 10-11-11.123'} m.createDate = {d '2021-08-21'} |
Boolean | TRUE, FALSE | |
Enum | 패키지명을 포함한 전체 이름을 사용 | jpabook.MemberType.Admin |
엔티티 타입 | 엔티티의 타입을 표현. 주로 상속고 관련해서 사용 | TYPE(m) = Member |
연산자 우선 순위
- 경로 탐색 연산(.)
- 수학 연산: +, -, *, ...
- 비교 연산: =, >, ≥ .. BETWEEN, LIKE, IN, IS NULL, EMPTY ...
- 논리 연산: NOT, AND ,OR
논리 연산과 비교식
- 논리 연산
- AND: 둘다 만족하면 참
- OR: 둘 중 하나만 만족해도 참
- NOT: 조건식의 결과 반대
- 비교식
- =, >, ≥, <, ≤, <>
Between, IN, Like, NULL 비교
X [NOT] BETWEEN A AND B
- X는 A~B 사이의 값이면 참(A, B값 포함)
//예제: 나이가 10~20인 회원을 찾아라 select m from Member m where m.age between 10 and 20
X [NOT] IN
- X와 같은 예제에 하나라도 있으면 참 (IN은 서브쿼리 사용 가능)
//예제: 이름이 회원1이나 회원2인 회원을 찾아라 select m from Member m where m.username in ('회원1', '회원2')
문자표현식 [NOT] LIKE 패턴값 [ESCAPE 이스케이프문자]
- 문자표현식과 패턴값을 비교
%(퍼센트)
: 아무 값들이 입력되어도 가능(값이 없어도 됨)
_(언더라인)
: 한 글자는 아무 값이 입력되지만 값이 있어야 함
//중간에 원이라는 단어가 들어간 회원(좋은회원, 회원, 원) select m from Member m where m.username like '%원%' //회원A, 회원1 where m.username like '회원_' //회원% where m.username like '회원\%' ESACAPE '\'
- 문자표현식과 패턴값을 비교
{단일값 경로 | 입력 파라미터} IS [NOT] NULL
- NULL인지 비교 NULL은 =비교는 안되고 꼭 IS NULL을 사용
where m.username is null where null = null //거짓 where 1=1 //참
컬렉션 식
컬렉션에만 사용하는 특별한 기능, 컬렉션 이외에서 사용 불가
{ 컬렉션 값 연관 경로 } IS [NOT] EMPTY
- 컬렉션에 값이 비었으면 참
//예제: 주문이 하나라도 있는 회원 조회 select m from Member m where m.orders is not empty //실행된 SQL select m.* from Member m where exists ( select o.id from Order o where m.id=0.member_id ) //is null은 컬렉션 식이 아니라 오류 발생 select m from Member m where m.orders is null //오류 발생
{ 엔티티나 값 } [NOT] MEMBER [OF] { 컬렉션 값 연관경로 }
- 엔티티나 값이 컬렉션에 포함되어 있으면 참
select t from Team t where :memberParam member of t.members
스칼라 식
숫자, 문자, 날짜, case, 엔티티 타입과 같은 기본적인 타입
- 수학 식
- +, - : 단항 연산자
- *, /, +, - : 사칙연산
문자함수
함수 | 설명 | 예제 |
---|---|---|
CONCAT(문자1, 문자2, ...) | 문자를 합한다 | CONCAT('A', 'B') = AB |
SUBSTRING(문자, 위치, [길이]) | 위치부터 시작해 길이만큼 문자를 구한다. 길이 값이 없으면 나머지 전체 길이를 구함 | SUBSTRING('ABCDEF', 2, 3) = BCD |
TRIM([LEADING | TRAILING | BOTH] [트림문자] FROM] 문자) | LEADING: 왼쪽만 TRAILING: 오른쪽만 BOTH: 양쪽 다 트림문자를 제거 기본값은 BOTH 트림 문자 기본값은 공백 (SPACE) | TRIM(' ABC ' ) = 'ABC' |
LOWER(문자) | 소문자로 변경 | LOWER('ABC') = 'abc' |
UPPER(문자) | 대문자로 변경 | UPPER('abc') = 'ABC' |
LENGTH(문자) | 문자 길이 | LENGTH('ABC') = 3 |
LOCATE(찾을 문자, 원본 문자, [검색시작위치]) | 검색위치부터 문자를 검색 1부터 시작, 못 찾으면 0 반환 | LOCATE('DE', 'ABCDEFG') = 4 |
수학함수
함수 | 설명 | 예제 |
---|---|---|
ABS(수학식) | 절대값을 구한다. | ABS(-10) = 10 |
SQRT(수학식) | 제곱근을 구한다. | SQRT(4) = 2.0 |
MOD(수학식, 나눌 수) | 나머지를 구한다. | MOD(4,3) = 1 |
SIZE(컬렉션 값 연관 경로식) | 컬렉션의 크기를 구한다. | SIZE(t.members) |
INDEX(별칭) | LIST 타입 컬렉션의 위치값을 구함 컬렉션이 @OrderColumn을 사용하는 LIST 타입일 때 사용 가능 | t.members m where INDEX(m) > 3 |
- 날짜 함수
- CURRENT_DATE: 현재 날짜
- CURRENT_TIME: 현재 시간
- CURRENT_TIMESTAMP: 현재 날짜 시간
select CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP from Team t //결과 2013-08-19, 23:38:17, 2013-08-19 23:38:17.736
- 하이버네이트는 날짜 타입에 년, 월, 일, 시간, 분, 초 기능 지원
- YEAR, MONTH, DAY, HOUR, MINUTE, SECOND
CASE 식
특정 조건에 따라 분기 할 때 CASE 식을 사용
- 기본 CASE
//문법 CASE {WHEN <조건식> THEN <스칼라식>} + ELSE <스칼라식> END //JPQL 예시 select case when m.age <= 10 then '학생요금' when m.age >= 60 then '경로요금' else '일반요금' end from Member m
- 심플 CASE
//문법 CASE <조건대상> {WHEN <스칼라식1> THEN <스칼라식2>} + ELSE <스칼라식> END //JPQL 예시 select case t.name when '팀A' then '인센티브110%' when '팀B' then '인센티브120%' else '인센티브105%' end from Team t
- COALESCE
- 스칼라식을 차례대로 조회해서 null이 아니면 반환
//문법 COALESCE (<스칼라식> {,<스칼라식>}+) //예시: m.username이 null이면 '이름 없는 회원'을 반환 select coalesce(m.username, '이름 없는 회원') from Member m
- NULLIF
- 두 값이 같으면 null을 반환하고 다르면 첫번 째 값을 반환 집합 함수는 null을 포함하지 않으므로 보통 집합 함수와 함께 사용
//문법 NULLIF(<스칼라식>, <스칼라식>) //예시 : 사용자 이름이 '관리자'면 null 반환, 나머지는 본인 이름 출력 select NULLIF(m.username, '관리자') from Member m
10.2.11 다형성 쿼리
- JPQL로 부모 엔티티를 조회하면 자식 엔티티도 함께 조회
다형성 쿼리 엔티티
@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "DTYPE") public abstract class Item {...} @Entity @DiscriminatorValue("B") public class Book extends Item { ... private String author; } //Album, Movie 생략
부모 조회
List resultList = em.createQuery("select i from Item i").getResultList();
- 단일 테이블 전략(
InheritanceType.SINGLE_TABLE
) 을 사용한 경우SELECT * FROM ITEM
- 조인 전략(
InheritanceType.JOINED
)을 사용한 경우SELECT I.ITEM_ID, ..., B.AUTHOR, ..., A.ARTIST, ..., M.ACTOR, ..., FROM ITEM I LEFT OUTER JOIN BOOK B ON I.ITEM_ID=B.ITEM_ID LEFT OUTER JOIN ALBUM A ON I.IOTEM_ID=A.ITEM_ID LEFT OUTER JOIN MOVIE M ON I.ITEM_ID=M.ITEM_ID
- 단일 테이블 전략(
TYPE
- 조회 대상을 특정 자식 타입으로 한정할 떄 주로 사용
//JPQL select i from Item i where type(i) IN (Book, Movie) //SQL SELECT I FROM ITEM I WHERE I.DTYPE IN ('B', 'M')
TREAT(JPA 2.1)
- 자바의 타입 캐스팅과 비슷한 기능
JPA 표준은 FROM, WHERE절만 사용 가능, 하이버네이트는 SELECT절도 사용 가능
//JPQL select i from Item i where treat(i as Book).author = 'kim' //SQL SELECT I.* FROM ITEM I WHERE I.DTYPE='B' AND I.AUTHOR='kim'
10.2.12 사용자 정의 함수 호출(JPA 2.1)
//문법
function_invocation:: = FUNCTION(function_name (, function_args}*)
//JPQL
select function('group_concat', i.name) from Item i
- 하이버네이트 구현체를 사용하면 방언 클래스 상속을 구현 후 함수 등록이 필요
//방언 클래스 상속 public class MyH2Dialect extends H2Dialect { pulbic MyH2Dialect() { registerFunction("group_concat", new StandardSQLFunction ("group_concat", StandardBasicTypes.STRING)); } } //hibernate.dialect에 해당 방언을 등록 <property name="hibernate.dialect" value="hello.MyH2Dialect" /> //하이버네이트 구현체 사용시 JPQL select group_concat(i.name) from Item i
10.2.13 기타 정리
- enum은 = 비교 연산만 지원
- 임베디드 타입은 비교를 지원하지 않음
EMPTY STRING
- JPA표준은 ''을 길이 0인 Empty String으로 정의 하지만 DB에 따라 ''을 NULL로 사용하는 데이터베이스도 있으므로 확인 필요
NULL 정의
- 조건을 만족하는 데이터가 하나도 없으면 NULL
- NULL과의 모든 수학적 계산 결과는 NULL
10.2.14 엔티티 직접 사용
기본 키 값
- 객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본 키 값으로 식별
//JPQL select count(m.id) from Member m //엔티티의 아이디를 사용 select count(m) from Member m //엔티티를 직접 사용 //실행결과 두개가 동일 select count(m.id) as cnt from Member m
엔티티를 파라미터로 사용하는 코드
//엔티티 String qlString = "select m from Member m where m = :member"; List resultList = em.createQuery(qlString) .setParameter("member", member) .getResultList(); //식별자 값 String qlString = "select m from Member m where m.id = :memberId"; List resultList = em.createQuery(qlString) .setParameter("memberId", 4L) .getResultList(); //실행된 SQL select m.* from Member m where m.id=?
외래 키 값
외래키 엔티티, 식별자 값 코드
//엔티티
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();
//식별자
String qlString = "select m from Member m where m.team.id = :teamId";
List resultList = em.createQuery(qlString)
.setParameter("teamId", 1L)
.getResultList();
//실행 결과 (두개가 동일)
select m.*
from Member m
where m.team_id=?
m.team.id
은 묵시적 조인처럼 보이지만 MEMBER 테이블이team_id
외래 키가 존재하여 묵시적 조인 발생 X
m.team.name
등 id가 아닌 값을 조인하면 묵시적 조인이 발생
10.2.15 Named 쿼리: 정적 쿼리
JPQL쿼리는 크게 동적 쿼리와 정적쿼리로 정의
- 동적 쿼리:
em.createQuery("select ....")
런타임에 특정 조건에 따라 JPQL을 동적으로 구성
- 정적 쿼리: 미리 정의한 쿼리에 이름을 부여하여 사용, 한번 정의하면 변경 못함 (Named 쿼리)
- 로딩 시점에 JPQL문법을 확인해 오류 확인이 편리, 재사용하므로 성능상도 이점
- @NamedQuery 어노테이션을 사용하거나 XML 문서에 작성하여 사용
Named 쿼리를 어노테이션에 정의
- Named 쿼리는 쿼리에 이름을 부여해서 사용하는 방법
@Entity //쿼리가 한개인 경우 @NamedQuery( name = "Member.findByUsername", query = "select m from Member m where m.username = :username") //쿼리가 2개 이상인 경우 @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 { ... }
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class) .setParameter("username", "회원1") .getResultList();
@NamedQuery
어노테이션 속성lockMode
: 쿼리 실행 시 락을 설정
hints
: SQL힌트가 아닌 JPA 구현체에게 제공하는 힌트 (예: 2차 캐시를 다룰때 사용)
Named 쿼리를 XML에 정의
- JPA에선 어노테이션으로 작성할 수 있는 것은 XML로 작성이 가능
- 어노테이션이 직관적이고 편리하지만 자바 언어로 멀티라인 문자를 다루는게 불편
"select" + "case t.name when '팀A' then '인센티브100%' " + ... "from Team t";
META-INF/ormMember.xml로 정의한 Named 쿼리
<xml version="1.0" encoding="UTF-8"?> <entity-mappings xmlns="https://xmlns.jcp.org/xm1/ns/persistence/orm" version="2.1"> <named-query name="Member.findByUsername"> <query><CDATA[ select m from Member m where m.username = :username ]></query> </named-query> <named-query name="Member.count"> <query>select count(m) from Member m </query> </named-query> </entity-mappings>
META-INF/persistence.xml에 설정
<persistence-unit name="jpabook"> <mapping-file>META-INF/ormMember.xml</mapping-file> ...
환경에 따른 설정
- XML과 어노테이션에 같은 설정이 있다면 XML이 우선권
- 예를 들어 같은 이름의 Named 쿼리가 있다면 XML에 있는 것을 사용
'개발서적 > 자바 ORM 표준 JPA' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 10.4 QueryDSL (0) | 2021.09.13 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 10.3 Criteria (0) | 2021.09.13 |
[자바 ORM 표준 JPA 프로그래밍] 10.1 객체체지향 쿼리 소개 (0) | 2021.09.13 |
[자바 ORM 표준 JPA 프로그래밍] 9.3~5 값 타입과 불변 객체, 값 비교, 값 타입 컬렉션 (0) | 2021.09.13 |
[자바 ORM 표준 JPA 프로그래밍] 9.1~2 기본값 타입, 임베디드 타입 (0) | 2021.08.25 |