[JPA 기본편] 10. 객체지향 쿼리 언어1 - 기본 문법
소개
JPA는 JPQL, JPA Criteria, QueryDSL, 네이티브 SQL, JDBC API 직접 사용 그리고 MyBatis, SpringJdbcTemaplate을 함께 사용하는 방법 등의 다양한 쿼리 방법을 지원한다.
JPQL
가장 단순한 방법으로 JPA를 사용하면 엔티티 객체를 중심으로 개발을 진행한다. 문제는 검색 쿼리인데, 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색하지만, 모든 DB 데이터를 개체로 변환해서 검색하는 것은 불가능하다. 따라서 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요하다.
- JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
- SQL과 문법이 유사하며, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 제공
- JPQL은 엔티티 객체를 대상으로 쿼리하고, SQL은 데이터베이스 테이블을 대상으로 쿼리
즉, JPQL은 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리로 SQL을 추상화해서 특정 DB SQL에 의존하지 않는다.
JPQL을 한마디로 정의하면 객체 지향 SQL
List<Member> result = em.createQuery(
"select m from Member m where m.username like '%kim%'", Member.class
).getResultList();
tx.commit();
Criteria
- 문자가 아닌 자바 코드로 JPQL을 작성할 수 있으며 JPQL 빌더 역할을 제공한다.
- JPA 공식 기능
너무 복잡하고 실용성이 없으므로 QueryDSL을 사용 권장함
// Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
Root<Member> m = query.from(Member.class);
CriteriaQuery<Member> cq = query.select(m);
// 사용하기 어렵지만 동적 쿼리로 만들기가 쉬움
String username = "def";
if (username != null) {
cq.where(cb.equal(m.get("username"), "kim"));
}
List<Member> resultList = em.createQuery(cq).getResultList();
tx.commit();
QueryDSL
- 문자가 아닌 자바 코드로 JPQL을 작성할 수 있으며, JPQL 빌더 역할을 한다.
- 컴파일 시점에 문법 오류를 찾을 수 있음
- 동적 쿼리 작성이 편리하고, 단순하고 쉬움
- 실무 사용 권장
// JPQL
// select m from Member m where m.age>18
JPAFactoryQuery query = new JPAQueryFactroy(em);
QMember m = QMember.member;
List<Memeber> list = query.selectFrom(m)
.where(m.age.gt(18))
.orderBy(m.name.desc())
.fetch();
네이티브 SQL
- JPA가 제공하는 SQL을 직접 사용하는 기능으로 JPQL로 해결할 수 없는 특정 DB에 의존적인 기능
String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();
JDBC 직접 사용, SpringJdbcTemplate 등
- JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, MyBatis 등을 함께 사용 가능하다.
- 단, 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요. (ex: JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시)
JPQL - 기본 문법과 쿼리 API
JPQL은 객체지향 쿼리 언어로 엔티티 객체를 대상으로 쿼리하며, SQL을 추상화해서 특정 DB SQL에 의존하지 않는다.
JPQL은 결국 SQL로 변환된다.
JPQL 문법 구조
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
- select m from Member as m where m.age > 18
- 엔티티와 속성은 대소문자 구분 O (Member, age)
- JPQL 키워드는 대소문자 구문 X (SELECT, FROM, where)
- 엔티티 이름 사용, 테이블 이름이 아님(Member)
- 별칭은 필수(m) (as는 생략 가능)
집합과 정렬
- GROUP BY, HAVING, ORDER BY 모두 사용 가능
select
COUNT(m), //회원수
SUM(m.age), //나이 합
AVG(m.age), //평균 나이
MAX(m.age), //최대 나이
MIN(m.age) //최소 나이
from Member m
TypeQuery, Query
- TypeQuery : 반환 타입이 명확할 때 사용
- Query : 반환 타입이 명확하지 않을 때 사용
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
// username이 String형
TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class);
// username과 age의 타입이 다르므로 TypedQuery 사용 불가 -> Qurey 사용
Query query3 = em.createQuery("select m.username, m.age from Member m", Member.class);
결과 조회 API
- query.getResultList() : 결과가 하나 이상일 때, 리스트 반환
- 결과가 없으면 빈 리스트 반환
- query.getSingleResult() : 결과가 정확히 하나여야 함. 단일 객체 반환
- 결과가 없으면? javax.persistence.NoResultException
- 결과가 둘 이상이면? javax.persistence.NonuniqueRequltException
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
Member result = query.getSingleResult();
System.out.println("result = " + result);
파라미터 바인딩 - 이름 기준, 위치 기준
위치 기반은 1, 2, 3 같이 숫자로 표시되며 중간에 하나를 끼워넣게 되면 순서가 밀리게 된다. 이러면 결국 장애로 이어지기 때문에 위치 기반은 사용하지 말고 이름 기반을 사용해라. 이름 기반은 위치가 바뀌어도 버그가 발생하지 않는다.
// 이름 기반
Member result = em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
System.out.println("result = " + result.getUsername());
프로젝션
SELECT 절에 조회할 대상을 지정하는 것으로 프로젝션 대상으로 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 될 수 있다. DISTINCT로 중복 제거할 수 있다.
- SELECT m FROM Member m → 엔티티 프로젝션
- SELECT m.team FROM Member m → 엔티티 프로젝션
- SELECT m.address FROM Member m → 임베디드 타입 프로젝션
- SELECT m.username, m.age FROM Member m → 스칼라 타입 프로젝션
SELECT m.username, m.age FROM Member m 처럼 여러 필드가 나열되어 있을 때는 세 가지 방법으로 조회할 수 있다.
1. Query 타입으로 조회
List resultList = em.createQuery("select m.username, m.age from Member m").getResultList();
Object o = resultList.get(0);
Object[] result = (Object[])o;
System.out.println("result = " + result[0]); // member1
System.out.println("result = " + result[1]); // 10
2. Object[] 타입으로 조회
List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m").getResultList();
Object[] result = resultList.get(0);
System.out.println("result = " + result[0]);
System.out.println("result = " + result[1]);
3. new 명령어로 조회
SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m 처럼 단순 값을 DTO로 바로 조회하는 것으로 패키지 명을 포함한 전체 클래스 명을 입력해야 하며, 순서와 타입이 일치하는 생성자가 필요하다.
List<MemberDTO> result = em.createQuery("select new hellojpa.jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class).getResultList();
MemberDTO memberDTO = result.get(0);
System.out.println("memberDTO = " + memberDTO.getUsername());
System.out.println("memberDTO = " + memberDTO.getAge());
페이징 API
JPA는 페이징을 다음 두 API로 추상화한다.
- setFirstResult(int startPositon) : 조회 시작 위치(0부터 시작)
- setMaxResults(int maxResult) : 조회할 데이터 수
for (int i = 0; i < 100; i++) {
Member member = new Member();
member.setUsername("member" + i);
member.setAge(i);
em.persist(member);
}
// 페이징 쿼리
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(1)
.setMaxResults(10)
.getResultList();
페이징 API에 따른 MySQL 방언과 Oracle 방언을 적용하여 실행한 결과는 아래와 같이 출력된다.
조인
- 내부 조인 : SELECT m FROM Member m [INNER] JOIN m.team t
- 외부 조인 : SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
- 세타 조인 : SELECT count(m) from Member m, Team t WHERE m.username = t.name (연관관계가 아예 없는걸 조회할 때 사용)
조인 - ON 절
ON 절을 활용한 조인 방법으로 JPA 2.1부터 지원된다.
- 조인 대상 필터링
- 예) 회원과 팀을 조인하면서, 팀 이름이 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'
- 연관관계 없는 엔티티 외부 조인
- 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인 (회원의 이름과 팀의 이름은 아무 연관이 없음)
- 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
서브 쿼리
서브 쿼리는 SQL의 서브 쿼리와 같은 것으로 아래의 예시처럼 쿼리 안에서 또 서브 쿼리를 만들 수 있는 것이다.
- 나이가 평균보다 많은 회원 : select m from Member m where m.age > (select avg(m2.age) from Member m2)
- 한 건이라도 주문한 회원 : select m from Member m where (select count(o) from Order o where m = o.member) > 0
서브 쿼리 지원 함수
- [NOT] EXISTS (subquery) : 서브쿼리에 결과가 존재하면 참
- {ALL | ANY | SOME} (subquery)
- ALL 모두 만족하면 참
- ANY, SOME : 같은 의미, 조건을 하나라도 만족하면 참
- [NOT] IN (subquery) : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
서브 쿼리 - 예제
- 팀 A 소속인 회원 : select m from Member m where exists (select t from m.team t where t.name = '팀A')
- 전체 상품 각각의 재고보다 주문량이 많은 주문들 : select o from Order o where o.orderAmout > ALL (select p.stockAmount from Product p)
- 어떤 팀이든 팀에 소속된 회원 : select m from Member m where m.team = ANY (select t from Team t)
JPA 서브 쿼리 한계
- JPA 표준에서는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능
- 하이버네이트에서는 SELECT 절에서도 사용 가능
FROM 절의 서브 쿼리는 현재 JPQL에서 불가능하며 조인으로 풀 수 있으면 풀어서 해결해야 한다.-> 하이버네이트6 부터는 FROM 절의 서브 쿼리를 지원한다.
JPQL 타입 표현
- 문자 : 'HELLO', 'She''s'
- 숫자 : 10L(Long), 10D(Double), 10F(Float)
- Boolean : TRUE, FALSE
- ENUM : jpabook.Member.Type.Admin (패키지 명 포함)
- 엔티티 타입 : TYPE(m) = Member (상속 관계에서 사용)
JPQL 기타
- SQL과 문법이 같은 식
- EXISTS, IN
- AND, OR, NOT
- =, >, >=, <, <=, <>
- BETWEEN, LIKE, IS NULL
조건식(CASE 등)
기본 CASE 식
String query = "select " +
"case when m.age <= 10 then '학생요금' " +
"case when m.age >= 60 then '경로요금' " +
"else '일반 요금' " +
"end " +
"from Member m";
List<String> result = em.createQuery(query, String.class)
.getResultList();
단순 CASE 식
select
case t.name
when '팀A' then '인센티브110%'
when '팀B' then '인센티브120%'
else '인센티브105%'
end
from Team t
COALESCE : 하나씩 조회해서 Null이 아니면 반환
// 사용자 이름이 없으면 이름 없는 회원을 반환
String query = "select coalesce(m.username, '이름 없는 회원') from Member m ";
List<String> result = em.createQuery(query, String.class)
.getResultList();
NULLIF : 두 값이 같으면 null 반환, 다르면 첫 번째 값 반환
// 사용자 이름이 '관리자'면 null을 반환하고 나머지는 본인의 이름을 반환
String query = "select nullif(m.username, '관리자') from Member m";
List<String> result = em.createQuery(query, String.class)
.getResultList();
JPQL 함수
JPQL 기본(표준) 함수
- CONCAT : 문자와 문자를 더한다. "select concat('a', 'b') from Member m";
- SUBSTRING : 잘라내는거. "select substring(m.username, 2, 3) from Member m";
- TRIM : 공백 제거
- LOWER, UPPER : 대소문자 변경
- LENGTH : 문자열 길이
- LOCATE : 찾는 문자열의 위치(인덱스)를 출력. "select locate('de', 'abcdegf') from Member m"; // 4
- ABS, SQRT, MOD : 수학 관련 함수
- SIZE, INDEX(JPA 용도. INDEX는 컬렉션의 위치 값을 구할 때 사용하는 것으로 안 쓰는 것이 좋다)
사용자 정의 함수 호출
하이버네이트는 사용 전 방언에 추가해야 한다. 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록하여 사용한다.
select function('group_concat', i.name) from Item i
[출처]
https://www.inflearn.com/course/ORM-JPA-Basic -> 이 글은 김영한님의 JPA 강의 중 10장을 듣고 정리한 내용입니다.