[강의] 자바 ORM 표준 JPA 프로그래밍 - 기본편 10 객체지향 쿼리 언어1- 기본 문법
자바 ORM 표준 JPA 프로그래밍 - 기본편 강의
김영한님의 인프런 강의(자바 ORM 표준 JPA 프로그래밍 - 기본편) 을 수강하면서 강의 내용을 일부 발췌해 요약한 글.
섹션 10 객체지향 쿼리 언어1 - 기본 문법
JPA는 다양한 쿼리 방법을 지원하는데, JPQL이라는 표준 문법을 기반으로 JPA Criteria와 QueryDSL는 자바 코드로 JPQL을 빌드해주는 제네레이터라고 생각할 수 있다. 표준 SQL 문법에 벗어나는, 특정 데이터베이스의 벤더에 종속적인 쿼리를 날려야 하는 경우 네이티브 SQL도 가능하며 JDBC API 직접 사용, MyBatis, SpringJdbcTemplate를 함께 사용하는 것도 가능하다.
아래에서 조금 더 자세히 다룬다.
JPQL(Java Persistence Query Language)
가장 단순한 조회 방법은 EntityManager.find() 였다. JPA를 사용하면 엔티티 객체를 중심으로 개발하게 되는데 결국 문제는 검색 쿼리이다. 검색 시에도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 하는데, 모든 DB 데이터를 객체로 변환해 검색하는 것은 불가능하다. 결국 필요한 데이터만 불러오려면 검색 조건이 포함된 SQL이 필요하다.
이러한 문제를 해결하기 위해 JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공한다.
JPQL은 SQL과 문법이 유사하나 데이터베이스 테이블을 대상으로 쿼리하는 SQL과는 달리 객체 를 대상으로 검색하며, SQL을 추상화하여 특정 데이터 베이스에 의존하지 않는다.
JPQL은 결국 SQL로 변환된다.
Criteria
문자가 아닌 자바코드로 JPQL을 작성할 수 있어 동적 쿼리가 어려운 경우에 사용하기 좋다. JPQL의 빌더 역할을 한다. 그러나 가독성이 좋지 못하고 복잡하여 유지보수가 어렵고 실용적이지 않아 실무에서는 잘 사용하지 않는다. (SQL스럽지 않다.) 이런게 있구나 정도만 알고 넘어가도 된다. Criteria 보다는 QueryDSL 사용을 권장한다.
QueryDSL
문자가 아닌 자바 코드로 JPQL을 작성할 수 있고, JPQL의 빌더 역할을 한다. 컴파일 시점에 문법 오류를 찾을 수 있다는 장점이 있으며 동적 쿼리 작성이 쉽고 직관적이다. 실무 사용에 권장된다.
Native SQL
JPQL 로는 해결할 수 없는 특정 데이터베이스에 의존적인 기능 필요로 할 때 사용하며 SQL을 직접 쿼리할 수 있도록 제공한다.
JDBC 직접 사용, SpringJdbcTemplate 등
JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나 SpringJdbcTemplate, MyBatis 를 함께 사용이 가능하다. 플러시는 커밋될 때 뿐만 아니라 EntityManager를 통해 쿼리가 날라갈 때도 동작한다. 따라서 JPA 관련 기술 사용시에는 문제가 되지 않지만, 이러한 경우에는 영속성 컨텍스트를 적절한 시점에 강제로 플러시할 필요가 있다.
JPQL 기분 문법
쿼리 한번에 여러 개를 업데이트할 때는 벌크 연산 이라고 해서 JPA에서는 따로 다룬다.
1
2
3
4
5
6
7
8
9
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
ex) select m from Member as m where m.age > 18
- 엔티티와 속성은 대소문자를 구분하며, JQPL 키워드는 대소문자를 구분하지 않는다.
- 엔티티의 이름을 사용하며 테이블 이름이 아니다!
- 별칭은 필수(m)이며, as는 생략이 가능하다.
- COUNT, SUM, AVG, MAX, MIN 사용 가능
- GROUP BY, HAVING, ORDER BY 사용 가능
- 반환타입이 명확한 경우 TypeQuery, 명확하지 않은 경우 Query를 사용한다.
1 2 3 4 5 6
//타입을 명기할 수 있는 경우 TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class); TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class); //타입을 명기할 수 없는 경우 Query query4 = em.createQuery("select m.username, m.age from Member m");
결과 조회 API
1
2
3
4
5
6
7
8
9
10
//결과가 하나 이상일 때
List<Member> resultList = query.getResultList();
for (Member member1 : resultList) {
System.out.println("member1 = " + member1);
}
//결과가 무조건 하나일 때
Member result = query.getSingleResult();
System.out.println("result = " + result);
query.getResultList(): 결과가 하나 이상일 때, 리스트를 반환하고 결과가 없으면 빈 리스트를 반환한다.
query.getSingleResult(): 결과가 정확히 하나일 때, 단일 객체 반환. 무조건 하나인 것이 보장된 경우에만 사용하자!!
- 결과가 없으면: javax.persistence.NoResultException
- 둘 이상이면: javax.persistence.NonUniqueResultException
파라미터 바인딩
이름과 위치를 기준으로 파라미터 바인딩이 가능하다. 그러나 위치는 변동이 쉬으므로 사용을 지양하고 이름 기준으로 사용하는 것이 좋다.
이름 기준 (:)
SELECT m FROM Member m where m.username=:username
query.setParameter("username", usernameParam);
위치 기준 (?)
SELECT m FROM Member m where m.username=?1
query.setParameter(1, usernameParam);
이름 기준의 예시는 다음과 같다.
1
2
3
4
5
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());
결과
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Hibernate:
/* select
m
from
Member m
where
m.username = :username */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as team_id4_0_,
member0_.username as username3_0_
from
Member member0_
where
member0_.username=?
singleResult = member1
프로젝션
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라고 한다. 엔티티, 임베디드 타입, 스칼라 타입(숫자나 문자 등 기본 데이터 타입)이 프로젝션의 대상이 된다.
엔티티 프로젝션
SELECT m FROM Member m
List<Member> result = em.createQuery("select m from Member m", Member.class).getResultList();
라는 쿼리를 날린다고 했을 떄, result는 영속성 컨텍스트에 반영된다! 값을 바꿔도 정상적으로 반영이 된다.
SELECT m.team FROM Member m
- 실행해보면 다음과 같은 쿼리가 날라간다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
Hibernate: /* select m.team from Member m */ select team1_.id as id1_3_, team1_.name as name2_3_ from Member member0_ inner join Team team1_ on member0_.TEAM_ID=team1_.id
참고로 여기서 jpql이 sql에 비해 간단해 좋은 것 아닐까? 라고 생각할 수 있으나 조인은 성능에 큰 영향을 주므로 명시적으로 작성하는 게 좋다! 예를 들어 “select t from Member m join m.team t” 와 같은 식으로 작성하여 join이 보이게 작성해주는 것이 좋다. 이와 관련된 것은 추후 경로 표현식에서 다룬다.
임베디드 타입 프로젝션
SELECT m.address FROM Member m
(Address는 임베디드 타입)
em.createQuery("select o.address from Order o", Team.class).getResultList();
와 같은 방식으로 사용한다. 그러나 임베디드 타입은 항상 소속되어 있기 때문에 from Address 같이 임베디드 타입만으로는 불러올 수 없다는 한계가 있다.
스칼라 타입 프로젝션
SELECT m.username, m.age FROM Member
DISTINCT로 중복 제거가 가능하다. ‘SELECT distinct m.username, m.age from Member m’ 일반 SQL Select 프로젝션과 가장 유사하다.
여러 값을 조회하는 경우 다음의 세 가지 방식으로 조회할 수 있다.
- Query 타입으로 조회
- Object[] 타입으로 조회
- new 명령어로 조회
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//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("username = " + result[0]);
System.out.println("age = " + result[1]);
//2. Object[] 타입으로 조회
List<Object []> resultList2 = em.createQuery("select m.username, m.age from Member m").getResultList();
Object[] result2 = resultList2.get(0);
System.out.println("username = " + result2[0]);
System.out.println("age = " + result2[1]);
//3. new 명령어로 조회
List<MemberDTO> result3 = em.createQuery("select new hellojpa.jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class).getResultList();
MemberDTO memberDTO = result3.get(0);
System.out.println("username = " + memberDTO.getUsername());
System.out.println("age = " + memberDTO.getAge());
3번의 경우 단순 값을 DTO로 바로 조회할 수 있으며 패키지명을 포함한 전체 클래스명을 new 명령어로 입력해주어야 한다. 순서와 타입이 일치하는 생성자가 필요하다.
페이징
JPA는 페이징을 다음의 두 API로 추상화할 수 있다.
- setFirstResult(int startPosition): 조회 시작 위치(0부터 시작)
- setMaxResults(int maxResult): 조회할 데이터 수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for(int i=0; i < 100; i++) {
Member member = new Member();
member.setUsername("member" + i);
member.setAge(i);
em.persist(member);
}
em.flush();
em.clear();
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(1)
.setMaxResults(10)
.getResultList();
System.out.println("result.size = " + result.size());
for (Member member1 : result) {
System.out.println("member1 = " + member1);
}
을 실행시키면 다음과 같은 쿼리가 실행된다.
Hibernate:
/* select
m
from
Member m
order by
m.age desc */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as team_id4_0_,
member0_.username as username3_0_
from
Member member0_
order by
member0_.age desc limit ? offset ?
이를 oracle dialect로 바꿔보면 다음과 같다. 이렇듯 복잡한 페이징 쿼리를 편하게 사용할 수 있다!
Hibernate:
/* select
m
from
Member m
order by
m.age desc */
select
*
from
( select
row_.*,
rownum rownum_
from
( select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as team_id4_0_,
member0_.username as username3_0_
from
Member member0_
order by
member0_.age desc ) row_ )
where
rownum_ <= ?
and rownum_ > ?
조인
내부 조인, 외부 조인, 세타 조인이 가능하다. 아래의 예제 쿼리에서 []는 생략이 가능하다.
• 내부 조인: 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절
JPA 2.1부터는 on절을 이용해 조인 대상을 필터링하거나 연관관계가 없는 엔티티를 외부 조인하는 것이 가능하다.
예를 들어, 회원과 팀을 조인하면서 팀 이름이 A인 팀만 조인하고 싶은 경우, SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
로 조회가 가능하다.
또한, 연관관계가 없는, 회원의 이름과 팀의 이름이 같은 대상을 외부 조인하고 싶은 경우 SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
로 가능하다.
서브 쿼리
나이가 평균보다 많은 회원, 한 건이라도 주문한 고객과 같이 쿼리 안에서 서브 쿼리를 만들 수 있다.
select m from Member m where m.age > (select avg(m2.age) from Member m2)
예시처럼 메인 쿼리와 서브 쿼리가 관계가 없이 짜야 성능이 잘 나온다.
서브쿼리에서 지원하는 함수로는 다음이 있다.
[NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
select m from Member m where exists (select t from m.team t where t.name = ‘팀A')
{ALL ANY SOME} (subquery) - ALL 모두 만족하면 참
select o from Order o where o.orderAmount > ALL (select p.stockAmount from Product p)
- ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
select m from Member m where m.team = ANY (select t from Team t)
- ALL 모두 만족하면 참
- [NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
JPA 서브 쿼리 한계
JPA는 WHERE, HAVING 절에서만 서브 쿼리가 가능하다. 하이버네이트에서는 SELECT도 지원한다. FROM 절의 서브 쿼리는 불가 하였으나 현재 하이버네이트 6에서 지원한다.
JPQL 타입 표현
예시 | |
---|---|
문자 | ‘HELLO’, ‘She”s” |
숫자 | 10L(Long), 10D(Double), 10F(Float) |
Boolean | TRUE, FALSE |
ENUM | jpabook.MemberType.Admin(패키지명을 모두 포함한다.) |
ENUM | TYPE(m) = Member (상속 관계에서 사용) |
EXISTS, IN, AND, OR, NOT .. 등 대부분 표준 SQL 문법을 대부분 지원한다.
조건식
- 기본 CASE식
1 2 3 4 5 6 7
String query = "select " + "case when m.age <= 10 then '학생요금'" + " when m.age >= 60 then '경로요금'" + " else '일반요금'" + "end" + " from Member m ";
- 단순 CASE식 : exact 매칭
- COALESCE : 하나씩 조회해서 null이 아니면 반환
String query = "select coalesce(m.username, '이름 없는 회원') from Member m";
- NULLIF : 두 값이 같으면 null을 반환하고, 다르면 첫번째 값을 반환
String query = "select nullif(m.username, '관리자') as username from Member m";
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’, ‘abcdef’) From Member m” | | ABS, SQRT, MOD | 수학 관련 | | SIZE | 컬렉션 사이즈 반환 “select size(t.members) From Team t” | | INDEX | JPA용도(@OrderColumn), 사용을 권장하지 않음 |
사용자 정의 함수
하이버네이트는 사용전 방언에 추가해야 한다.
dialect를 만들고, 사용하는 dialect를 상속받는다.
1
2
3
4
5
6
7
8
9
10
11
package dialect;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.type.StandardBasicTypes;
public class MyH2Dialect extends H2Dialect {
public MyH2Dialect() {
registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
}
}
persistence.xml 파일도 변경해준다.
1
<property name="hibernate.dialect" value="dialect.MyH2Dialect">
이제 select function('group_concat', m.username) from Member m
를 할 수 있다.