티스토리 뷰
| JPQL(Java Persistence Query Language)
- 테이블이 아닌 엔티티 객체를 대상으로 검색하는 객체지향 쿼리
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음
- JPA는 JPQL을 분석한 후 적절한 SQL을 만들어 데이터베이스를 조회
- 방언(Dialect)만 변경하면 JPQL을 수정하지 않고 자연스럽게 DB 변경 가능
> 회원 엔티티
1 2 3 4 5 6 7 | @Entity(name = "Member") public class Member { @Column(name = "name") private String username; // ... } | cs |
> JPQL
- 엔티티 이름과 엔티티 객체의 필드 명으로 작성
1 2 | String jpql = "select m from Member as m where m.username = 'kim'"; List<Member> resultList = em.createQuery(jpql, Member.class).getResultList(); | cs |
|| 기본 문법
JPQL 문법의 전체 구조는 SQL과 유사
||| SELECT 문
1 | SELECT m FROM Member AS m WHERE m.username = 'KIM' | cs |
> 대소문자 구분
- 엔티티와 속성은 대소문자 구분
- JPQL 키워드는 구분하지 않음
> 엔티티 이름
- 테이블 명 대신 엔티티 명을 사용, @Entity(name=" ") 으로 설정 가능
- 지정하지 않을 시 클래스 명을 기본값으로 사용(기본값을 추천)
> 별칭은 필수
- JPQL은 별칭을 필수
- AS는 생략 가능
||| TypeQuery, Query
JPQL 실행 시 쿼리 객체 생성이 필요
ㅇ TypeQuery : 반환할 타입을 명확하게 지정할 수 있을 경우
1 2 3 4 5 6 7 8 | TypedQuery<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); } | cs |
ㅇ Query : 반환 타입을 명확하게 지정할 수 없을 경우
,여러 엔티티나 컬럼을 선택할 경우(반환 타입이 명확하지 않을 경우) 사용
1 2 3 4 5 6 7 8 9 | 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[] System.out.println("username = " + result[0]); System.out.println("age = " + result[1]); } | cs |
||| 결과 조회
ㅇ query.getResultList() : 결과를 예제로 반환
- 결과가 없을 경우 빈 컬렉션 반환
ㅇ query.getSingleResult() : 결과가 정확히 하나일 때 사용
- 결과가 없으면 javax.persistence.NoResultException 예외 발생
- 결과가 1보다 많으면 javax.persistence.NonUniqueResultException 예외 발생
|| 파라미터 바인딩
ㅇ 이름 기준 파라미터(Named parameters)
- 파라미터를 이름으로 구분
- : 사용
- 더 명확한 방식
1 2 3 4 5 6 7 8 9 10 11 12 13 | String usernameParam = "User1"; TypedQuery<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(); | cs |
ㅇ 위치 기준 파라미터(Positional parameters)
- ? 다음에 위치 값을 지정
1 2 3 4 | List<Member> member = em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class) .setParameter(1, usernameParam) .getResultList(); | cs |
* 파라미터 바인딩 방식은 필수!
|| 프로젝션(projection)
- SELECT 절에 조회할 대상을 지정하는 것.
.엔티티
.엠비디드 타입
.스칼라 타입(숫자, 문자 등 기본 타입)
||| 엔티티 프로젝션
- 원하는 객체를 바로 조회
- 엔티티 프로젝션으로 조회한 엔티티는 영속성 컨텍스트에서 관리
1 2 | SELECT m FROM Member m // 회원 SELECT m.team FROM Member m // 팀 | cs |
||| 임베디드 타입 프로젝션
- 엔티티와 거의 비슷하게 사용
- 조회의 시작점이 될 수 없음
- 엔티티 타입이 아닌 값 타입. 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않음
1 2 3 | String query = "SELECT o.address FROM Order o"; List<Address> addresses = em.createQuery(query, Address.class) .getResultList(); | cs |
1 2 3 4 5 6 | select order.city, order.street, order.zipcode from Orders order | cs |
||| 스칼라 타입 프로젝션
- 숫자, 문자, 날짜와 같은 기본 데이터 타입
1 2 3 | List<String> username = em.createQuery("SELECT username FROM Member m", String.class) .getResultList(); | cs |
||| 여러 값 조회
- 여러 값 선택 시 Query 사용
ㅇ 여러 프로젝션
1 2 3 4 5 6 7 8 9 10 | Query query = em.createQuery("SELECT m.username, m.age, FROM Member m"); List resultList = query.getResultList(); Iterator iterator = resultList.iterator(); while (iterator.hasNext()) { Object[] row = (Object[]) iterator.next(); String username = (String) row[0]; Integer age = (Integer) row[1]; } | cs |
ㅇ 여러 프로젝션 Object[] 조회
1 2 3 4 5 6 7 8 | List<Object[]> resultList = em.createQuery("SELECT m.username, m.age FROM Member m") .getResultList(); for (Object[] row : resultList) { String username = (String) row[0]; Integer age = (Integer) row[1]; } | cs |
ㅇ 여러 프로젝션 엔티티 타입 조회
1 2 3 4 5 6 7 8 9 | 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]; // 스칼라 } | cs |
||| New 명령어
- SELECT 다음 NEW 명령어 사용 시 반환받을 클래스 지정 가능,
이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있음
- New 명령어를 사용한 클래스로 TypeQuery 사용이 가능하여 객체 변환 작업에 효율적
> 명령어 사용 시 주의사항
1. 패지키 명을 포함한 전체 클래스 명 기입
2. 순서와 타입이 일치하는 생성자 필요
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class UserDTO { private String username; private int age; public UserDTO(String username, int age) { this.username = username; this.age = age; } //... } TypeQuery<UserDTO> query = em.createQuery("SELECT new test.jpql.UserDTO(m.username, m.age) FROM Member m", UserDTO.class); List<UserDTO> resultList = query.getResultList(); | cs |
|| 페이징 API
- setFirstResult (int startPosition) : 조회 시작 위치(zero base)
- setMaxResults (int maxResult) : 조회할 데이터 수
- 데이터 방언(Dialect) 덕분에 DB마다 다른 페이징 처리를 같은 API로 처리
1 2 3 4 5 6 7 8 | TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class); // 11~30번 데이터 조회 query.setFirstResult(10); // 조회 시작 위치 query.setMaxResults(20); // 조회할 데이터 수 query.getResultList(); | cs |
|| 집합과 정렬
||| 집합 함수
COUNT
MAX, MIN
AVG
SUM
> 참고 사항
- null 값은 무시
- 값이 없는데 집합 함수 사용 시 null. 단, count는 0
- DISTINCT를 COUNT에서 사용 시 임베디드 타입은 지원 X
||| GROUP BY, HAVING
- 통계 데이터를 구할 때 특정 그룹끼리 묶어줌.
- 보통 전체 데이터를 기준으로 처리하므로 실시간으로 사용하기에 부담
- 결과가 아주 많을 경우 통계 결과만 저장하는 테이블을 별도로 만들어 두고 사용자가 적은 새벽에 통계 쿼리를 실행
||| 정렬(ORDER BY)
- 결과 정렬 시 사용
|| JPQL 조인
||| 내부 조인
- INNER JOIN 사용
- INNER는 생략 가능
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 |
ㅇ 서로 다른 타입의 두 엔티티 조회 시
1 2 3 4 5 6 7 | List<Object[]> result = em.createQuery("SELECT m, t FROM Member m JOIN m.team t") .getResultList(); for (Object[] row : result) { Member member = (Member) row[0]; Team team = (Team) row[1]; } | cs |
||| 외부 조인
1 2 | SELECT m FROM Member m LEFT [OUTER] JOIN m.team t | cs |
||| 컬렉션 조인
- 일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것.
* 다대일 조인 : 단일 값 연관 필드(m.team)
* 일대다 조인 : 컬렉션 값 연관 필드(m.members)
1 2 | SELECT t, m FROM Team t LEFT JOIN t.members m | cs |
||| 세타 조인
- WHERE 절을 사용한 세타 조인 (내부 조인만 지원)
- 전혀 관계없는 엔티티도 조인 가능
1 2 3 | SELECT count(m) FROM Member m, Team t WHERE m.username = t.name | cs |
||| JOIN ON
- 조인 대상 필터링 (내부조인의 ON 절은 WHERE 절 대체 가능 -> 외부 조인에서만 사용)
1 2 3 | SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A' | cs |
|| fetch Join
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 한 번에 같이 조회 (JPQL은 결과를 반환할 때 연관관계까지 고려하지 않음 -> fetch join)
ㄴ SQL 호출 횟수를 줄여 성능 최적화
ㄴ 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않음
ㄴ 준영속 상태에서도 객체 그래프를 탐색
- 글로벌 로딩 전략보다 우선
> fetch join의 한계
- 페치 조인 대상에는 별칭을 줄 수 없음
- 둘 이상의 컬렉션을 페치할 수 없음 (카타시안 곱 발생)
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없음 (단일 값 연관 필드(일대일, 다대일)는 가능)
||| 엔티티 fetch join
- join fetch 라고 기입하면 연관된 엔티티나 컬렉션을 함께 조회
- fetch join 은 별칭을 사용할 수 없음
* 아래 결과는 회원 엔티티만 선택했는데 회원과 연관된 팀도 함께 조회
ㅇ SQL Query
1 2 | select m from Member m join fetch m.team | cs |
1 2 3 4 5 6 7 8 9 10 | String jpql = "select m from Member m join fetch m.team" List<Member> members = em.createQuery(jpql, Member.class) .getResultList(); for (Member member : members ) { // 페치 조인으로 회원과 팀을 함께 조회해서 자연 로딩 발생 안 함 System.out.println(member.getUsername()); System.out.println(member.getTeam().name()); } | cs |
||| 컬렉션 페치 조인
- 일대다 컬렉션 패치 조인
* 아래 결과는 TEAM 테이블에서 '팀A'는 하나지만 MEMBER 테이블과 조인하면서 결과가 증가
- 데이터가 아닌 객체 관점이므로 A팀에 2명의 회원이 있다면, 총 4개의 데이터가 조회되는 현상이 발생
(distinct 추가)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | String jpql = "select distinct t from TEAM t join fetch t.members where t.name = '팀A'" List<Team> teams = em.createQuery(jpql, Team.class) .getResultList(); for (Team team : teams) { System.out.println(team.getName() + ", " + team); for (Member member : team.getMambers()) { // fetch join으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안 함 System.out.println(member.getUsername() + ", " + member); } } | cs |
|| 경로 표현식
* 상태 필드 (state field) : 단순히 값을 저장하기 위한 필드(필드 or 프로펄티)
* 연관 필드 (association field) : 연관관계를 위한 필드, 임베디드 타입 포함(필드 or 프로펄티)
- 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티
- 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션
* 경로 탐색을 사용한 묵시적 조인 시
- 항상 내부 조인
- 컬렉션에서 경로 탐색을 할 경우 명시적으로 조인해서 별칭을 얻어야 함
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Entity public class Member { @Id @GeneratedValue private Long id; @Column(name = "name") private String username; // 상태 필드 private Integer age; // 상태 필드 @ManyToOne(..) private Team team; // 연관 필드 (단일 값 연관 필드) @OneToMany(..) private List<Order> orders; // 연관 필드 (컬렉션 값 연관 필드) } | cs |
* 상태 필드 경로 : 경로 탐색의 끝 (더 이상 탐색 불가)
1 2 | select m.username, m.age from Member m | cs |
* 단일 값 연관 경로 : 묵시적으로 내부 조인 발생 (계속 탐색 가능)
1 2 | select o.member from Order o | cs |
* 컬렉션 값 연관 경로 : 묵시적으로 내부 조인 발생 (더 이상 탐색 불가)
단, 경로 탐색을 할 경우 FROM절에서 조인을 통해 별칭 획득 필요
컬렉션 크기를 구할 수 있는 size 기능 존재 (select t.members.size from Team t)
1 2 3 4 5 6 7 | select t.member from Team t -- 컬렉션에서 경로 탐색을 할 경우 select m.username from Team t join t.members m | cs |
|| 서브 쿼리
* SQL과 다르게 WHERE, HAVING 절에서만 사용 가능
||| 서브 쿼리 함수
* [NOT] EXISTS (subquery)
1 2 3 4 5 6 | -- 팀 A소속 회원 select m from Member m where exists (select t from m.team t where t.name = '팀A') | cs |
* [All | ANY | SOME] (subquery)
1 2 3 4 5 | -- 전체 상품 각각의 재고보다 주문량이 많은 주문들 select o from Order o where o.orderAmount > ALL (select p.stockAmount from Product p) | cs |
* [NOT] IN (subquery)
1 2 3 4 5 6 | -- 20세 이상을 보유한 팀 select t from Team t where t IN (select t2 from Team t2 JOIN t2.members m2 where m2.age >= 20) | cs |
|| 조건식
ㅇ 타입 표현
[] 문자 : 'hello'
[] 숫자 : 10L (Long)
10D (Double)
10F (Float)
[] 날짜 : {d '2020-12-22'} (Date)
{t '13-39-55'} (Time)
{ts '2020-12-22 13-39-55.123'} (DateTime)
[] Boolean : true, false
[] Enum : test.MemberType.Enum (패키지 명을 포함한 전체 이름)
[] 엔티티 타입 : TYPE(m) = Member
ㅇ 연산자 우선 순위
ㅇ 논리 연산과 비교식
ㅇ Between, IN, Like, NULL
- Between
1 2 3 4 | -- 나이가 10~20인 회원 select m from Member m where m.age between 10 and 20 | cs |
- [NOT] IN
1 2 3 4 | -- 이름이 회원1이나 회원2인 회원 select m from Member m where m.username in ('회원1', '회원2') | cs |
- Like
- IS [NOT] NULL
ㅇ 컬렉션 식
- 빈 컬렉션 비교 : IS [NOT] EMPTY
1 2 3 4 | -- 주문이 하나라도 있는 회원 select m from Member m where m.orders if not empty | cs |
ㅇ 스칼라 식
- 문자 함수 : CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE
- 수학 함수 : ABS, SQRT, MOD, SIZE, INDEX
- 날짜 함수 : CURRENT_DATE(현재 날짜), CURRENT_TIME(현재 시간), CURRENT_TIMESTAMP(현재 날짜 시간)
ㅇ CASE 식
- CASE, COALESCE, NULLIF
|| 다형성 쿼리
ㅇ TYPE
- 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용
1 2 3 4 | -- Item 중 Book, Movie를 조회 select i from Item i where type(i) IN (Book, Movie) | cs |
ㅇ TREAT
- 상속 구조에서 부모 타입을 특정 자식 타입으로 캐스팅
1 2 3 | select i from Item i where treat(i as Book).author = 'kim' | cs |
|| 엔티티 직접 사용
ㅇ 기본 키 값
- JPQL에서 엔티티 객체(Member)를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값(Member.id)을 사용
ㅇ 외래 키 값
|| Named 쿼리: 정적 쿼리
ㅇ 동적 쿼리
- JPQL을 문자로 완성해서 직접 넘기는 것
ㅇ 정적(Named) 쿼리
- 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용
- 한 번 정의하면 변경할 수 없는 정적인 쿼리
||| Named 쿼리
- 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱 -> 오류를 빨리 발견
- 사용 시점에 파싱된 결과를 재사용 -> 성능상 이점
- 정적 SQL 생성 -> DB 죄회 성능 최적화
- @NamedQuery Annotation으로 자바 코드에 작성하거나 XML 문서에 작성할 수 있음
|||| @NamedQuery Annotation
1. @NamedQuery.name에 쿼리 이름 부여
@NamedQuery.query에 사용할 쿼리 입력
1 2 3 4 5 6 7 | @Entity @NamedQuery ( name = "Member.findByUsername", query="select m from Member m where m.username = :username") public class Member { // ... } | cs |
1 2 3 4 5 6 7 8 9 10 11 12 | @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 |
1 2 3 | List<Member> resultList = em.createNamedQuery("Member.findByUsername", Mamber.class) .setParameter("username", "회원1") .getResultList(); | cs |
|||| Named 쿼리를 XML에 정의
Named 쿼리 작성 시 XML을 사용하는 것이 편리
1. XML 작성
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?xml version="1.0" encoding="UTF-8"?> <entity-mappings xmlns="http://java.sun.com/xml/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> | cs |
2. 정의한 xml 파일을 인식할 수 있도록 persistence.xml 에 아래 코드 추가
- XML이 Annotation보다 우선권
1 2 3 4 5 6 7 8 | <?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.1"> <persistence-unit name="test"> <mapping-file> META-INF/Member.xml </mapping-file> </persistence-unit> </persistence> | cs |
출처 : 자바 ORM 표준 JPA 프로그래밍
'Books' 카테고리의 다른 글
[JPA] 네이티브 SQL 정리 (0) | 2020.12.23 |
---|---|
[JPA] QueryDSL 정리 (0) | 2020.12.22 |
[JPA] 고급 매핑(상속관계, 복합키, 식별/비식별, 조인 테이블) (0) | 2020.12.21 |
[JPA] 다양한 연관관계 매핑 (0) | 2020.12.21 |
[JPA] 연관관계 매핑이란.? (0) | 2020.12.19 |