강의를 들으며 생각 정리 + "자바 ORM 표준 JPA 프로그래밍" 책 참고
소개
EntityManager.find() 메소드를 사용하면 식별자로 엔티티 하나를 조회할 수 있다. 이렇게 조회한 엔티티에 객체 그래프 탐색(a.getB() 등)을 사용하면 연관된 엔티티들을 찾을 수 있다.
그러나 만약 나이가 30살 이상인 회원을 모두 검색하고 싶다면 find 메소드만으로 해결되지 않는다. 결국 SQL로 필요한 내용을 최대한 걸러서 조회해야 한다. 하지만 JPA는 데이터베이스 테이블이 아닌 객체를 대상으로 개발하므로 검색도 테이블이 아닌 객체를 대상으로 하는 방법이 필요하다.
JPQL은 이런 문제를 해결하기 위해 만들어졌는데 다음과 같은 특징이 있다.
- 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리다.
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
SQL이 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리라면 JPQL은 엔티티 객체를 대상으로 하는 객체지향 쿼리다. JPA는 이 JPQL을 분석한 다음 데이터베이스에 따라 적절한 SQL을 만들어 데이터베이스를 조회한다. JPQL은 SQL과 문법이 매우 유사하기 때문에 쉽게 적응할 수 있다.
JPA는 JPQL뿐만 아니라 Criteria 쿼리 등 다양한 검색 방법을 제공하고 JPA가 공식 지원하는 기능은 아니지만 QueryDSL 등 쿼리를 편하게 작성하도록 도와주는 프레임워크가 있다. 그러나 이러한 것들은 JPQL을 편하게 작성하도록 도와주는 빌더 클래스일 뿐이다. 따라서 JPQL을 이해해야 나머지를 이해할 수 있기 때문에 JPQL을 정확히 학습하는 것이 중요하다.
기본 문법과 쿼리 API
JPQL의 특징을 다시 정리해 보자.
- JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환된다.
문법
JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있따. UPDATE, DELETE 문은 벌크 연산이라고 하는데 나중에 알아보고 지금은 SELECT 문을 자세히 알아본다.
SELECT 문은 다음과 같이 사용한다.
select m from Member as m where m.age > 18
대소문자 구분
엔티티와 속성은 대소문자를 구분한다. 예를 들어 Member, age는 대소문자를 구분한다. 반면에 select, from, as 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
엔티티 이름
JPQL에서 사용한 Memeber는 클래스 명이 아니라 엔티티 명이다. 엔티티 명은 @Entitiy(name = "XXX")로 지정할 수 있다. 엔티티 명을 지정하지 않으면 클래스 명을 기본값으로 사용한다.
별칭
JPQL은 Member as m처럼 Member에 m이라는 별칭을 주어야 한다. as는 생략할 수 있다.
TypeQuery, Query
작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다. 쿼리 객체는 반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 객체를 사용하고, 반환 타입을 명확하게 지정할 수 없으면 Query 객체를 사용하면 된다.
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
List<Member> resultList = query.getResultList();
for (Member member1 : resultList) {
System.out.println("member1.getUsername() = " + member1.getUsername());
}
예시처럼 em.createQuery()의 두 번째 반환할 타입을 지정하면 TypeQuery를 반환한다.
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]);
}
위 예시는 조회 대상이 String 타입인 회원 이름과 Integer 타입인 나이이므로 조회 대상 타입이 명확하지 않다. 이 때는 Query 객체를 반환한다.
Query 객체는 SELECT 절의 조회 대상이 예제처럼 둘 이상이면 Object[]를 반환하고 조회 대상이 하나면 Object를 반환한다.
결과 조회
다음 메소드들을 호출하면 실제 쿼리를 실행해서 데이터베이스를 조회한다.
- query.getResultList() : 결과를 리스트로 반환한다. 만약 결과가 없으면 빈 리스트를 반환한다.
- query.getSingResult() : 결과가 정확히 하나일 때 사용한다.
- 결과가 없으면 NoResultException 예외가 발생한다.
- 결과가 1개보다 많으면 NoUniqueResultException 예외가 발생한다.
파라미터 바인딩
이름 기준 파라미터
TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = :username", Member.class);
query.setParameter("username", "member1");
List<Member> resultList = query.getResultList();
이름 기준 파라미터는 파라미터를 이름으로 구분하는 방법이다. 이름 기준 파라미터는 앞에 ':'를 사용한다. :username이라는 이름 기준 파라미터를 정의하고 query.setParameter()에서 username이라는 이름으로 파라미터를 바인딩한다.
위치 기준 파라미터
List<Member> members = em.createQuery("select m from Member m where m.username = ?1", Member.class)
.setParameter(1, "member1")
.getResultList();
(참고로 JPQL은 위와 같이 메소드 체인 방식으로 작성할 수 있다.)
위치 기준 파라미터를 사용하려면 '?' 다음에 위치 값을 주면 된다. 위치 값은 1부터 시작한다.
위치 기준 파라미터 방식보다는 이름 기준 파라미터 방식을 사용하는 것이 더 명확하다.
프로젝션(SELECT)
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 하고 [SELECT {프로젝션 대상} FROM]으로 대상을 선택한다. 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다. 스칼라 타입은 숫자, 문자 등 기본 데이터 타입을 뜻한다.
엔티티 프로젝션
select m from Member m
select m.team from Member m
처음은 회원을 조회했고 두 번째는 회원과 연관된 팀을 조회했다. 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
임베디드 타입 프로젝션
String query = "select o.address from Order o";
List<Address> addresses = em.createQuery(query, Address.class).getResultList();
JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용된다. 단, 임베디드 타입은 "from Address a"와 같이 조회의 시작점이 될 수 없다는 제약이 있다.
임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 영속성 컨텍스트에서 관리되지 않는다.
스칼라 타입 프로젝션
List<String> usernames = em.createQuery("select distinct m.username from Member m", String.class).getResultList();
숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다. 참고로 중복 데이터를 제거하려면 위와 같이 DISTINCT를 사용한다.
여러 값 조회
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];
}
프로젝션에 여러 값을 선택하면 Query를 사용해야한다. 다만, 예시와 같이 List<Object[]>를 사용하면 더 간결하게 개발할 수 있다.
NEW 명령어
TypedQuery<MemberDTO> query = em.createQuery("select new jpql.MemberDTO(m.username,m.age) from Member m", MemberDTO.class);
List<MemberDTO> resultList = query.getResultList();
<MemberDTO.java>
package jpql;
public class MemberDTO {
private String username;
private int age;
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
//Getter, Setter
}
이전에는 여러 값을 조회하기 위해 Object[]를 반환받았는데 이번에는 MemberDTO처럼 의미 있는 객체로 변환한다.
SELECT 다음에 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있어 TypeQuery 사용을 할 수 있다.
NEW 명령어를 사용할 때는 다음 2가지를 주의해야 한다.
- 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
- 순서와 타입이 일치하는 생성자가 필요하다.
페이징
JPA는 페이징을 다음 두 API로 추상화했다.
- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작한다)
- setMaxResults(int maxResult) : 조회할 데이터 수
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(0)
.setMaxResults(10)
.getResultList();
0 ~ 9번 데이터를 조회하는 예시이다. JPQL은 데이터베이스 방언에 따라 다른 페이징 처리를 할 수 있다.
조인
JPQL도 조인을 지원하는데 SQL 조인과 기능은 같고 문법만 약간 다르다.
내부 조인
내부 조인은 INNER JOIN을 사용한다. INNER는 생략할 수 있다.
String teamName = "teamA";
String query = "select m from Member m join m.team t "
+ "where t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
.setParameter("teamName", teamName)
.getResultList();
JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다. 예시에서는 m.team이 연관 필드인데 연관 필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말한다.
외부 조인
외부 조인은 LEFT OUTER JOIN을 사용한다. OUTER는 생략할 수 있다.
select m from Memer m left join m.team t
세타 조인
WHERE 절을 사용해서 세타 조인을 할 수 있다. 기본적으로 내부 조인이다.
String query = "select count(m) from Member m, Team t where m.username = t.name";
List<Long> members = em.createQuery(query, Long.class)
.getResultList();
세타 조인을 사용하면 예시처럼 전혀 관계없는 엔티티도 조인할 수 있다.
(ex. member1은 teamA와 연관되어 있고, member2는 teamA와 연관되어 있지 않다. 그러나 memebr1, member2의 username과 teamA의 name이 같다면 teamA와 관계없는 member2 역시 count된다.)
JOIN ON 절
JPA 2.1부터 조인할 때 ON 절을 지원한다. ON 절을 사용하면 조인 대상을 필터링하고 조인할 수 있다. 또한, 연관관계 없는 엔티티를 외부 조인할 수 있다.(하이버네이트 5.1부터)
조인 대상 필터링
String query = "select m from Member m left join Team t on t.name= :teamName";
이처럼 조인 대상을 필터링할 수 있다. on 대신 where를 쓰면 문법 오류가 난다.
연관관계 없는 엔티티 외부 조인
기존 세타 조인은 내부 조인이었지만 on 절을 사용하여 연관관계가 없는 엔티티를 외부 조인할 수 있다.
String query = "select m, t from Member m left join Team t on m.username=t.name";
on절의 조건을 만족하지 않는 회원과 팀에 대해 팀은 null 값을 갖게 된다.
서브 쿼리
JPQL도 SQL처럼 서브 쿼리를 지원한다. 단, 서브 쿼리를 WHERE, HAVING 절에서만 사용할 수 있고, SELECT, FROM 절에서는 사용할 수 없다. (하이버네이트는 SELECT 절의 서브 쿼리도 허용한다.)
다음은 나이가 평균보다 많은 회원을 찾는다.
String query = "select m from Member m where m.age > (select avg(m2.age) from Member m2)";
다음은 한 건이라도 주문한 고객을 찾는다.
String query = "select m from Member m where (select count(o) from Order o where m=o.member)>0";
서브 쿼리 함수
EXISTS : 서브 쿼리에 결과가 존재하면 참이다.
팀A 소속인 회원
String query = "select m from Member m where exists (select t from m.team t where t.name='teamA')";
ALL : 조건을 모두 만족하면 참이다.
ANY or SOME : 조건을 하나라도 만족하면 참이다.
전체 상품 각각의 재고보다 주문량이 많은 주문들
String query = "select o from Order o where o.orderAmount > ALL(select p.stockAmount from Product p)";
어떤 팀이든 팀에 소속된 회원
String query = "select m from Member m where m.team = ANY(select t from Team t)";
JPQL 타입 표현과 기타식
JPQL은 여러 타입을 지원한다.
문자
'Hello', 'She''s'
숫자
10L(Long), 10D(Ddouble), 10F(Float)
Boolean
TRUE, FALSE
ENUM
jpql.MemberType.ADMIN
엔티티 타입
Type(m) = Member : 상속 관계에서 사용
다음은 여러 타입을 활용한 예시이다.
String query = "select m.username, 'HELLO', TRUE from Member m "+
"where m.type = jpql.MemberType.ADMIN";
List<Object[]> result = em.createQuery(query).getResultList();
for (Object[] objects : result) {
System.out.println("objects[0] = " + objects[0]);
System.out.println("objects[1] = " + objects[1]);
System.out.println("objects[2] = " + objects[2]);
}
ENUM 타입의 경우 패키지명을 전부 넣어줘야 한다는 것을 기억하자. 단, setParameter()로 ENUM 타입을 받는 경우 패키지명을 생략해도 된다.
만약 상속 관계를 사용한다면(Book -> Item) 엔티티 타입을 사용할 수 있다.
String query = "select i from Item i where type(i)=Book";
JPQL은 SQL과 문법이 같은 여러 식을 지원한다.
- EXISTS, IN
- AND, OR, NOT
- BETWEEN, LIKE, IS NULL
조건식(CASE 등등)
JPQL은 특정 조건에 따라 분기할 때 CASE 식을 사용한다.
기본 CASE 식
String query = "select " +
"case when m.age<=10 then '학생요금' " +
"when m.age>=60 then '경로요금' " +
"else '일반요금' " +
"end " +
"from Member m";
단순 CASE 식
String query = "select " +
"case t.name " +
"when 'teamA' then '100원' " +
"when 'teamB' then '200원' " +
"else '0원' " +
"end " +
"from Team t";
COALESCE
: 하나씩 조회해서 null이 아니면 반환
String query = "select coalesce(m.username,'이름 없는 회원') from Member m";
m.username이 있으면 반환하고 없으면 '이름 없는 회원'을 반환한다.
NULLIF
: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
String query = "select nullif(m.username,'관리자') from Member m";
JPQL 함수
JPQL 기본 함수
JPQL이 제공하는 표준함수로 데이터베이스에 상관 없이 호출 가능하다. 대표적인 함수를 소개한다.
CONCAT
String query = "select concat('a', 'b') from Member m";
문자열을 연결한다. 하이버네이트는 'a' || 'b' 표현도 지원한다.
SUBSTRING
String query = "select substring(m.username, 2, 3) from Member m";
문자열을 특정 인덱스의 부분 문자열로 나눈다.
LOCATE
String query = "select locate('de', 'abcdefg') from Member m";
우측 문자열 내에 좌측 문자열이 있다면 시작 인덱스를 반환한다.(Integer) 예시에서는 4를 반환한다.
SIZE
String query = "select size(t.members) from Team t";
컬렉션의 size를 반환한다.
이 밖에 여러 기본 함수들이 있다.
사용자 정의 함수
하이버네이트 구현체를 사용하면 방언 클래스를 상속해서 구현하고 사용할 데이터베이스 함수를 미리 등록해야 한다.
public class MyH2Dialect extends H2Dialect {
public MyH2Dialect() {
registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
}
}
그리고 hibernate.dialect에 해당 방언을 등록해야 한다.
<persistence.xml>
<property name="hibernate.dialect" value="dialect.MyH2Dialect"/>
그리고 다음과 같이 JPQL을 사용하면 된다.
String query = "select function('group_concat', m.username) from Member m";
+) group_concat은 회원들의 username을 연결해준다.
'java > jpa' 카테고리의 다른 글
[JPA] 프로젝트 환경설정 (0) | 2021.02.10 |
---|---|
[JPA] 객체지향 쿼리 언어2 - 중급 문법 (0) | 2021.02.08 |
[JPA] 값 타입 (0) | 2021.02.02 |
[JPA] 프록시와 연관관계 관리 (0) | 2021.01.29 |
[JPA] 고급 매핑 (0) | 2021.01.27 |