JPQL 이란?
JPQL의 필요성
- 일반적인 조회(
em.find()
, 객체그래프 탐색)가 아닌, 조건이 들어간 조회가 필요하다면? - 모든 데이터를 조회하고 앱단에서 걸러야 하나? → NO!
- 필요한 데이터만 DB에서 불러 오는 것! (객체 중심으로 조회!) → 이 때 결국 검색 조건이 포함된 SQL이 필요 ⇒ JPQL 사용 필요!
JPQL
- Java Persistence Query Language
- SQL을 추상화한 객체 지향 쿼리 언어
- SQL과 문법이 유사함 → SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN, …
- 핵심은 Entity 객체를 대상으로 쿼리를 한다는 것 (SQL은 데이터베이스 테이블을 대상으로 쿼리)
⇒ JPQL을 SQL로 번역하여 실제 DB로 보냄
-
JPQL
// String jpql = "select m from Member m where m.age > 18"; List<Member> result = em.createQuery( "select m from Member m where m.age > 18", Member.class).getResultList();
.cerateQuery()
: JPQL을 통해 query 생성jpql
: Entity 객체를 중심으로 query를 생성할 수 있음select m
: 기존 SQL같은 경우 내가 조회할 항목(Col)들을 모두 작성해주어야 하지만 JPQL은 Entity 중심이기에 해당 Entity로 반환할 수 있음
Member.class
: 반환할 Entity 지정
-
JPQL에 따라 실행된 SQL query
select m.id as id, m.age as age, m.USERNAME as USERNAME, m.TEAM_ID as TEAM_ID from Member m where m.age>18
-
- 즉, 복잡한 쿼리를 사용할 때 발생할 수 있는 JPA와 DB의 패러다임 차이를 극복해주는 것
- 실무에서 사용되는 복잡한 쿼리를 객체 지향적으로 짤 수 있도록 지원 → 즉, 조회 시 DB Table이 아닌 Entity 객체를 대상으로 검색
- JPA가 지원하는 다양한 쿼리 방법 중 하나
- JPA가 지원하는 다양한 쿼리
- 자바 코드를 통해 JPQL을 동적으로 빌드해주는 Genterator (동적으로 query를 변경하게 할 수 있음)
- JPA Criteria : JPA 표준 스펙. 사용이 복잡하며 실용성이 없음
- QueryDSL : 오픈 소스. 실무에서 동적인 query를 짤 때 자주 사용하는 기술
- Native SQL : JPQL을 사용해도 DB 종속적인 코드가 필요한데, 그 때 SQL 문법 그대로 사용할 수 있게끔 지원하는 것
- 자바 코드를 통해 JPQL을 동적으로 빌드해주는 Genterator (동적으로 query를 변경하게 할 수 있음)
- JPA가 지원하는 다양한 쿼리
- JPQL은 JDBC API 직접 사용 or MyBatis, SpringJdbc와 함께 사용이 가능함
- SQL을 추상화하기 때문에 특정 데이터베이스 SQL에 의존X
JPQL 기본 문법
기본 문법과 쿼리 API
- JPQL 기본 query
-
SELECT 문
select _ from _ [where _] [group by _] [having _] [order by _]
-
UPDATE 문
update _ [where _]
- 벌크 연산과 연관 : JPA의 한건한건에 대한 변경감지에 의해 진행되는 update가 아닌, 여러건을 한방에 UPDATE하는 것
-
DELETE 문
delete _ [where _]
- 기존 SQL과 굉장히 유사!
- 하지만 JPQL은 Entity 객체를 대상으로 Query!!
-
- JPQL 기본 문법
select m from Member as m where m.age > 18
- SQL과 거의 동일하지만, Member가 Table을 대상으로 하는 것이 아니라 Entity 객체를 대상으로 한다는 것이 중요 → 그래서 m.age 처럼 속성값을 바로 사용할 수 있는 것
- 또한 조회 자체를 Entity 객체로 반환할 수 있음
- 엔티티와 속성은 대소문자 구분O (Member, age)
- JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)
- [중요] 별칭은 필수(m) (as는 생략가능)
-
집합과 정렬
select COUNT(m), -- 회원 수 SUM(m.age), -- 나이 합 AVG(m.age), -- 평균 나이 MAX(m.age), -- 최대 나이 MIN(m.age) -- 최소 나이 from Member m
-
Group by, Having Order by 도 가능
-
- TypedQuery vs Query [쿼리 반환]
- TypedQuery : 반환 타입이 명확할 때 사용
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
- Select의 대상이 member entity 이므로 반환 타입이 명확함
- Query : 반환 타입이 명확하지 않을 때 사용
Query query = em.createQuery("SELECT m.username, m.age from Member m");
- m.username 은 String이고 m.age 는 int 로 두 개의 타입이 다르고, 이를 매핑할 타입이 존재하지 않음 → 반환 타입이 명확하지 않음
- TypedQuery : 반환 타입이 명확할 때 사용
- 결과 조회 API (Query API)
.getResultList()
: 조회 결과가 하나 이상일 때 → 리스트로 반환- 결과 없을 시 빈 리스트 반환
.getSingleResult()
: 결과가 정확히 하나일 때 → 단일 객체로 반환- 결과가 없을 시 오류 발생 →
javax.persistence.NoResultException
(Spring Data JPA(Spring과의 통합 JPA) 에서는 Null or Optional로 반환할 수 있도록 설정 됨) - 결과가 둘 이상일 시 오류 발생 →
javax.persistence.NonUniqueResultException
- 결과가 없을 시 오류 발생 →
-
파라미터 바인딩
em.createQuery( "SELECT m FROM Member m where m.username=:username", Member.class) .setParameter("username", usernameParam); // usernameParam 은 변수
- 바인딩 시 “이름 기준”과 “위치 기준” 이 존재 하지만 “이름 기준”만 사용하는 것을 권장 (위의 예시는 “이름 기준” 바인딩)
:username
: 파라미터 설정 → “:
”.setParameter
: 파라미터 바인딩
프로젝션 (SELECT)
- SELECT 절에 조회할 대상을 지정하는 것
- 대상
- Entity 객체 프로젝션
em.createQuery("SELECT m FROM Member m", Member.class);
- 조회되는 모든 Entity가 영속성 컨텍스트에 의해 관리됨
- Entity 연관 객체 프로젝션
em.createQuery("SELECT m.team FROM Member m",Team.class);
- SQL단에서 JOIN 절 자동 실행 (묵시적 JOIN) → DB입장에선 Member의 Team은 join해주어야 가져올 수 있음
- 좋아 보이지만 예기치 못한 query(JOIN) 이기에 사용하지 않는 것이 좋음 → SQL과 비슷하게 join을 직접 사용해서 가져오는 것을 권장 (명시적 JOIN)
- Embedded Type 객체 조회
em.createQuery("SELECT m.address FROM Member m",Address.class);
- 값 타입이기에 묵시적 JOIN이 실행되지 않음 → DB입장에선 해당 Col이 그대로 있기 때문
- 그저 반환이 Embedded Type으로 나오는 것
From Address a
와 같이 직접 조회는 불가능 → Entity가 아님! 소속되어 있는 객체임
- Scala Type(int,Long,String, … 등 기본 데이터 타입) 조회
em.createQuery("SELECT m.username, m.age FROM Member m");
- 해당 결과는 여러 Type(String, int)으로 조회됨 → 여러값 조회 방법 사용
- Entity 객체 프로젝션
- Scala Type에서의 여러 값 조회
- Query 타입으로 조회
- Query 타입(반환 타입이 명확하지 않은 상태)으로 받아온 후 Object로 빼오기
Object singleResult = em.createQuery("select m.name, m.age from Member m").getSingleResult(); Object[] resultOf = (Object[]) singleResult; for (Object o : resultOf) { System.out.println("result = " + o); }
-
Object[] 타입으로 조회
List<Object[]> resultList = em.createQuery("select m.name, m.age from Member m").getResultList(); for (Object[] ol : resultList) { for (Object o : ol) { System.out.println("result = ", o); } }
- new 명령어로 조회 (DTO 사용) → BEST!!
- DTO로 바로 조회
- 패키지 명을 포함한 전체 클래스 명을 입력해줘야 함
- 해당 조회되는 항목의 순서와 타입이 일치하는 생성자 필수
-
DTO 생성
public class MemberDto { private String name; private int age; public MemberDto(String name, int age) { this.name = name; this.age = age; } ... // getter setter }
-
new 명령어 조회
List<MemberDto> resultList = em.createQuery("select new hellojpql.dto.MemberDto(m.name, m.age) from Member m", MemberDto.class).getResultList(); for (MemberDto memberDto : resultList) { System.out.println("memberDto = " + memberDto.getName() + ", " + memberDto.getAge()); }
- Query 타입으로 조회
- DISTINCT로 중복제거 가능
- SQL 단의 DISTINCT : 완전히 일치하는 row에 대한 중복 제거
- JPA 단의 DISTINCT : 같은 식별자(Primary Key)를 가진 Entity에 대한 중복 제거
- JPQL에서의 DISTINCT 는 두 기능을 모두 작동하여 중복 제거 실행
페이징 API
- 페이징은 MySQL, ORACLE 등을 이용해서 직접 query를 짜려면 서로 다른 점도 있고 복잡함 (특히 ORACLE의 페이징은 굉장히 복잡함!) → But JPA는 SQL Dialect 존재
- 하지만 JPA는 페이징을 두가지의 API로 추상화하여 쉽게 이용 가능
.setFirstResult()
: 조회 시작 위치 (0부터 시작).setMaxResult()
: 조회할 데이터 수
-
예시
em.createQuery("select m from Member m order by m.age desc", Member.class) .setFirstResult(1) // offset 자동 추가 (offset = 1) .setMaxResults(10) // limit 자동 추가 (limit = 10 -> 10개) .getResultList(); // order by member.age desc limit ? offset ?
- .
setFirstResult(1)
- 두번째 데이터부터 가져온다는 뜻 (인텍스 0부터 시작)
- SQL로 보면 offset →
offset 1
을 자동 추가 해주는 것
setMaxResults(10)
- 10개의 데이터를 가져온다는 뜻 → 2번째부터 10개의 데이터를 가져옴 (11번재 데이터까지 조회)
- SQL로 보면 limit →
limit 10
을 자동 추가 해주는 것
- 결과 ⇒
select m from Member m order by member.age desc limit ? offset ?
-
결과 SQL
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 10 OFFSET 1
- .
JOIN
- 다양한 JOIN 지원
- 내부 조인
SELECT m FROM Member m [INNER] JOIN m.team t
- team이 없는 Member에 대해선 제외하고 조회됨
- 외부 조인
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
- team이 없는 Member에 대해선 Null로 설정된 채로 조회됨
- 세타 조인
select count(m) from Member m, Team t where m.username = t.name
- 연관관계에 있지 않은 것들을 JOIN하는 것 (일명 막조인)
- member가 3개고 team이 2개면 3x2의 row를 생성하는 join (일명 Cross JOIN)
- 내부 조인
- ON 절을 활용하여 JOIN
- JOIN 대상 필터링
- on 을 통해 JOIN 시 조건을 걸어 원하는 대상만 JOIN할 수 있도록 필터링 하는 것
- on t.name = ’A’ ⇒ ON m.TEAM_ID = t.id AND t.name = ‘A’
- JPQL ⇒
em.createQuery(”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'
- 연관관계 없는 Entity 외부 조인
- on 을 통해 서로 아무 연관관계가 없는 Entity라도 Cross Join이 가능
- JPQL ⇒
em.createQuery(”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
- JOIN 대상 필터링
서브 쿼리
- 쿼리 속에 또 다른 쿼리를 만들어 적용하는 것
- 예시
-
나이가 평균 초과인 회원
select m from Member m where m.age > (select avg(m2.age) from Member m2)
- 서브 쿼리 :
(select avg(m2.age) from Member m2)
- 메인 쿼리와 서브 쿼리는 Entity를 공용으로 사용하면 안됨 → m2를 m으로 바꿔써 메인쿼리의 Entity를 공용으로 사용하면 X
- 서브 쿼리 :
-
최소 한 건이라도 주문한 회원
select m from Member m where (select count(o) from Order o where m = o.member) > 0
- 서브 쿼리 :
(select count(o) from Order o where m = o.member)
- 서브 쿼리 :
-
- 지원 함수
**[NOT] EXISTS (subquery)**
: 서브쿼리의 결과가 존재하면 참- ex) Team A 소속인 회원 →
String query = "select m from Member m where exists (select t from m.team t where t.name = 'TeamA')";
- ex) Team A 소속인 회원 →
**{ALL | ANY | SOME} (subquery)**
- ALL : 모두 만족하면 참
- ex) 전체 상품 각각의 재고보다 주문량이 많은 주문들 →
select o from Order o where o.orderAmount > ALL (select p.stockAmount from Product p)
- ex) 전체 상품 각각의 재고보다 주문량이 많은 주문들 →
- ANY, SOME : 조건을 하나라도 만족하면 참
- ex) 어떤 팀이든 팀에 소속된 회원 →
select m from Member m where m.team = ANY (select t from Team t)
- ex) 어떤 팀이든 팀에 소속된 회원 →
- ALL : 모두 만족하면 참
**[NOT] IN (subquery)**
: 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
- JPA 서브 쿼리의 한계
- JPA 표준 에서는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능
- 하이버네이트 구현체에서는 SELECT 절도 사용 가능! →
select (select … )
- FROM 절의 서브쿼리는 JPQL에선 불가능!
- JOIN 으로 (풀 수 있으면) 풀어서 해결하는 것이 방법
- 혹은 Native query로 직접 SQL문을 짜는 것도 방법
JPQL 타입 표현
- 문자 : 작은따옴표 활용 →
‘Hello’
,‘She’’s’
- 숫자 : 자바 기본 숫자 표현 활용 →
10L(Long)
,10D(Double)
,10F(Float)
- Boolean :
TRUE
,FALSE
- ENUM : 패키지명 포함하여 활용 →
jpabasic.StudyState.STUDYING
-
파라미터를 활용하는 것도 가능 (자바 코드로 검증하는 것)
String query = "select m from Member m where m.state = :studyState"; em.createQuery(query, Member.class) .setParameter("studyState", StudyState.STUDYING) .getResultList();
-
- Entity type : 타입 일치 여부 확인 (상속 관계에서 사용) →
TYPE(m) = Member
- 다형성을 자주 활용한다면 사용 가능
em.createQuery(”select i from Item i where type(i) = Book”, Item.class);
→ DTYPE으로 판단해 줌
조건식 (CASE 식)
- 기본 CASE 식
- 범위에 해당 하면 발동
select case when m.age <= 10 then 'student' when m.age >= 30 then 'adult' else 'normal' end from Member m
- 단순 CASE 식
- 정확히 맞아 떨어지면 발동
select case t.name when 'TeamA' then '+110%' when 'TeamB' then '+120%' else '+105%' end from Team t
- COALESCE
- 하나씩 조회해서 null이면 정해진 값으로 반환
select coalesce(m.username,'No Name') from Member m
- NULLIF
- 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
select NULLIF(m.username, 'AMIN') from Member m
JPQL 기본 함수
- JPQL 표준 함수
- CONCAT : 문자 더하기 →
CONCAT('a', 'b')
or'a' || 'b'
- SUBSTRING : 문자 자르기 →
SUBSTRING(m.name, 2, 4)
- TRIM : 공백 제거
- LOWER, UPPER : 대소문자 로 변경
- LENGTH : 길이
- LOCATE : 위치 찾기 →
LOCATE(’de’, ‘abcdefgh’)
→ 4 - ABS, SQRT, MOD : 수학 함수
- SIZE (JPA 용도) : 컬렉션의 크기 →
SELECT SIZE(t.members) FROM Team t;
- INDEX (JPA 용도) : @OrderColumn 사용 시의 INDEX 확인용
- CONCAT : 문자 더하기 →
- 사용자 정의 함수
- 사용 전 방언에 추가해주어야 함 (hibernate 구현체는)
- 사용하는 DB Dialect를 상속받고, 사용자 정의 함수를 등록
- 예시
- H2 DB를 사용하면서 사용자 정의 함수를 등록하고자 할 때 (함수는 이미 만들었다고 가정)
-
방언 생성 후 등록
// 방언 생성 public class MyH2Dialect extends H2Dialect { public MyH2Dialect() { regiterFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING)); } }
<!-- 등록 --> <property name="hibernate.dialect" value="org.hibernate.dialect.MyH2Dialect"/>
-
사용
em.createQuery("select function('group_concat', i.name) from Item i")