이번에는 객체 지향 쿼리 언어에 대해 알아 볼 것이다.
양이 정말 많으니, 필요한 부분만 골라서 보는 것을 추천한다.
객제지향쿼리는 테이블이 아닌 객체를 대상으로 검색하는 쿼리이고, SQL을 추상화 했기 때문에 특정 데이이터베이스 SQL에 의존하지 않는다 라는 특징이 있다.
JPA는 객체지향 SQL로 JPQL, Creteria 쿼리 , Native SQL 을 공식적으로 지원한다.
그런데 QueryDSL , JDBC 직접 사용, SQL 매퍼 프레임워크 사용도 알면 좋다.
이번에는 이 중 가장 근본인 JPQL에 대해 중점적으로 살펴보고, 나머지는 다음 시간에 살펴보겠다.
JPQL 도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있는데, 저장할 때는 persist()를 사용하면 되니 INSERT 문은 없다.
기본 구조는 아래와 같다.
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 :: =update_절 [where_절]
delete_문 :: = delete_절 [where_절]
전체적은 구조는 SQL과 비슷하여 익숙하다.
SELECT m FROM Member AS m where m.username = 'BURGER'
작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다.
반환 타입이 명확하면 -> TypeQuery 객체를
명확하지 않으면 -> Query 객체를 사용하면 된다.
TypeQuery<Member> qery = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = query.getResultList();
//Member로 바로 받을 수 있다.
Query query = em.createQuery("SELECT m.username, m.age from Member m");
List resultList = query.getResultList();
//Query의 경우 SELECT 절의 조회 대상이 둘 이상이면, Object[] 를,
// 하나면 Object를 반환한다.
for(Object o : resultList){
Object[] result = (Object[]) o;
System.out.println("username = " +result[0]);
System.out.println("age = " +result[1));
}
//이렇게 값을 확인 할 수 있다.
이때
query.getResultList() 는 결과를 List 컬렉션으로 (결과가 없으면 빈 컬렉션을 반환) 반환한다.
query.getSingleResult()는 결과가 정확히 하나일 때 사용하며,
결과가 없으면 javax.persistence.NoResultException 예외가 발생하고
결과가 1개 초과이면 javax.persistence.NonUniqueResultException이 발생한다.
TypeQuery<Member> query = em.createQuery ("SELECT m FROM Member m where m.username = :username",Member.class);
query.setParameter("username","User1"); //바인딩
List<Member> members =
em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class)
.setParameter(1, "User1")
.getResultList();
//참고로 JPQL API 는 대부분 메소드 체인 방식으로 설계되어 있어,
//이렇게 연속해서도 작성가능하다.
파라미터 바인딩 방식은 선택이 아닌 필수!
직접 문자를 더해 만들면 SQL 인젝션 공격에 취약!
그리고 파라미터 바인딩을 사용하면 JPA 는 파라미터의 값이 달라도 같은 쿼리로 인식해서 JPQL 을 SQL로 파싱한 결과를 재사용 할 수도 있음!
-> 직접 하드코딩 하지 말고, 파라미터 바인딩 방식을 사용하자!
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션(projection)이라고 한다.
엔티티 프로젝션
SELECT m FROM Member m
위와 같이 원하는 객체를 바로 조회한 것이다. 여기서 컬럼을 나열애서 조회해야 하는 SQL과는 차이가 있는데, 이렇게 조회된 엔티티는 영속성 컨텍스트에서 관리된다.
임베디드 타입 프로젝션
임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.
SELECT a FROM Address a ->잘못된 예시
SELECT o.address FROM Order o ->올바른 예시
임베디드 타입은 엔티티 타입이 아닌 값 타입이기에, 영속성 컨텍스트에서 관리되지 않는다.
스칼라 타입 프로젝션
숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라고 한다.
SELECT DISTINT username From Member m ->중복 데이터를 제거한 문자 타입 조회 예시
여러 값 조회
프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없다.
List<Object[]> resultList =
em.createQuery("SELECT m.username, m.age FROM Member m").getResultList();
참고로 엔티티 타입도 여러 값을 함께 조회할 수 있다.
여러 값 조회에서 본 예시에서, 실무에서는 Object[]를 사용하기 보다는 이 값들을 담을 UsertDTO 를 만들고, Object[]를 UserDTO 객체로 변환해서 사용할 것이다.
하지만 일일히 객체를 변환하는 작업은 반복적이고 수고스럽다.
NEW 명령어를 사용하면 간단하게 변환할 수 있다.
TypeQuery<UserDTO> query =
em.createQuery("SELECT new jpabook.jpql.USERDTO(m.username, m.age) FROM Member m",UserDTO.class);
여기서
1. 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
2. 순서와 타입이 일치하는 생성자가 필요하다.
를 주의하자.
데이터 베이스마다 페이징을 처리하는 SQL 문법은 다르고, 처리용 SQL을 작성하는 건 정말 피곤하다.
JPA는 페이징을 다음과 같이 추상화하였다.
TypeQuery<Member> query =
em.createQuery("SELECT m From Member m ORDER BY m.username DESC", Member.class);
query.setFirstResult(10);
query.setMaxResults(20);
query.getREsultList();
이렇게 사용 할 수 있다.
만약 페이징 SQL을 최적화하고 싶다면, 네이티브 SQL을 직접 사용해야 한다.
집합 함수
select
COUNT(m), //회원수
SUM(m.age), //나이합
AVG(m.age), //평균나이
MAX(m.age), //최대나이
MIN(m.age) //최소나이
from Member m
SQL과 크게 다르지 않다.
여기서 AVG의 반환 타입은 Double이다.
** 집합 함수 사용 시 참고
NUlLL 값은 무시하므로 통계에 잡히지 않음.
값이 없는데 SUM, MIN 함수 등을 사용하면 NULL 값이 됨. 단 COUNT 는 0이 됨.
DISTINCT 를 COUNT 에서 사용할 때 임베디드 타입은 지원하지 않음.
GROUP BY, HAVING, ORDER BY 형식도 SQL과 크게 다르지 않다.
예시 코드를 보면 쉽게 이해할 수 있을 것이다.
select COUNT(m.age) FROM Member m LEFT JOIN m.team t GROUP BY t.name ORDER BY m.username ASC
마찬가지로 조인도 SQL 조인과 크게 다르지 않다.
1. 내부 조인
SELECT m FROM Member m INNER JOIN m.team t
INNER는 생략 가능한데 JPQL 조인의 특징은 위처럼 연관 필드를 사용하는 것이다.
SELECt Member m JOIN Team t
처럼 SQL 조인처럼 사용하면 안된다. 반드시 연관 필드를 이용해 조인을 해야 한다.
2. 외부 조인
SELECT m FROM Member m LEFT OUTER JOIN m.team t
여기서 OUTER 는 생략가능하여 보통 LEFT JOIN으로 사용한다.
3. 컬렉션 조인
[회원 ->팀] 처럼 다대일 조인일 경우 단일 값 연관 필드 (m.team)을 사용하고,
[팀->회원] 처럼 일대다 조인이면 컬렉션 값 연관 필드 (t.members)를 사용한다.
SELECT t, m FROM TEAM t LEFT JOIN t.member s
4. 세타 조인
WHERE 절을 사용해서 세타 조인을 할 수 있는데, 내부 조인만 지원한다.
select count(m) from Member m, Team t where m.username = t.name
세타 조인을 사용하면, Member.username, Team.name 를 통해 조인한 것 처럼 전혀 관계없는 엔티티도 조인할 수 있다.
5. JOIN ON 절
JPA 2.1부터 ON 절을 지원하는데, ON 절을 사용하면 조인 대상을 필터링 한 후 조인할 수 있다.
참고로 내부 조인의 경우 WHERE 절을 사용할 때와 결과가 같으므로 보통 외부 조인에서만 사용한다.
select m, t from Member m left join m.team t on t.name = 'A'
->모든 회원을 조회하면서, 연관된 팀도 조회하는데 이때 팀은 이름이 A 인 팀만 조회함
6. 페치 조인
JPQL에서 성능 최적화를 위해 제공하는 기능이다. 연관된 엔티티나 컬레션을 한 번에 같이 조회하는 기능인데, join fetch 명령어로 사용할 수 있다.
6.1 엔티티 페치 조인
select m from Member m join fetch m.team
이렇게 하면 연관된 팀까지 함께 조회된다.
그리고 m.team 다음에 별칭이 없는데 페치 조인은 별칭을 사용할 수 없다.🙅
연관된 팀까지 함께 조회되기 때문에 지연 로딩은 발생하지 않는다.
그리고 회원 엔티티가 영속성 컨텍스트에서 분리가 되어도 연관된 팀은 조회가 가능하다.
6.2 컬렉션 페치 조인
select t from Team t join fetch t.members
이때 실행된 SQL는 아래와 같다.
SELECT T., M.
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
만약 TEAM_ID 가 1인 회원이 2명이 있다면
ID | NAME | ID | TEAM_ID(FK) | NAME |
---|---|---|---|---|
1 | 팀A | 1 | 1 | 회원 |
1 | 팀A | 2 | 1 | 회원 |
위와 같이 조회된다.
팀A는 하나지만 MEMBER 테이블과 조인하면서 2건이 조회되었다.
그래서 위 쿼리를 실행하여 얻은 teams의 결과를 보면 주소가 0x100으로 같은 팀A를 2건 가지게 된다.
(같은 주소를 가진, 회원을 2명 가진 팀 객체가 2개가 들어가게 됨)
6.3 페치 조인과 DISTINCT
select distinct t from Team t join fetch t.members
이렇게 하면, 위에서는 중복이었던 팀A가 teams에 한 개만 들어가게 된다.
💃 여기서 잠깐 알아가기 : 페치조인과 일반 조인의 차이
지연로딩과 즉시로딩의 차이와 비슷한데,
일반 조인은 연관된 엔티티도 바로 조회하지 않고 그저 프록시나 초기화 되지 않은 컬렉션 래퍼를 반환한다.
6.4 페치 조인의 특징과 한계점
페치 조인은 글로벌 로딩 전략 (@OneToMany(fetch=FetchType.Lazy 는 글로벌 로딩 전략이다) 보다 우선한다.
글로벌 로딩 전략이 지연 로딩이어도 페치 조인을 사용하면 즉시 로딩이 된다.
따라서 글로벌 로딩 전략을 즉시 로딩으로 설정하기보다는, 지연 로딩을 사용하고 최적화가 필요할 때 페치 조인을 사용하는 것이 성능상 효과적이다.
(앞선 장들을 다 읽었다면 이해 될 것이다.)
특징
1. 페치 조인 대상에는 별칭을 줄 수 없다. 따라서 SELECT , WHERE , 서브 쿼리에 페치 조인 대상을 사용할 수 없다.
2. 둘 이상의 컬렉션을 페치할 수 없다. 일부 구현체는 가능한데, 카테시안 곱이 되니 주의해야 한다.
3. 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
JPQL에서는 경로 표현식이라는 것도 사용하는데, .을 찍어 객체 그래프를 탐색하는 형식이다.
select m.username from Member m
여기서 m.username이 경로 표현식에 해당한다.
용어 잠깐 정리하고 갑시다
상태 필드 : 단순히 값을 저장하기 위한 필드 (ext private String username 같은)
연관필드에서
단일 값 연관 필드 : @ManyToOne, @OntToOne 처럼 대상이 엔티티
컬렉션 값 연관 필드: @OntToMany, @ManyToMany처럼 대상이 컬렉션
상태 필드 경로의 경우 경로 탐색의 끝이다 (m.username에서 무엇을 더 탐색할 수 있겠는가)
단일 값 연관 경로의 경우 묵시적 내부 조인이 일어나는데 계속 탐색할 수 있다.
컬렉션 값 연관 경로도 묵시적 내부 조인이 일어나는데 더는 탐색할 수 없다.
그런데 FROM절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색을 더 할 수 있다.
** 여기서 묵시적 조인이란 JOIN을 직접 적어주는 것이 아니라
SELECT m.team FROM Member m
처럼 경로 표현식에 의해 묵시적으로 조인이 일어나는 것을 말한다.
상태 필드 경로는 앞선 예시 코드에서도 확인할 수 있었으니, 단일 값 연관 경로 탐색부터 사용 예시를 알아보겠다.
1. 단일 값 연관 경로 탐색
select o.member.team
from Order o
where o.product.name ='productA;
위의 JPQL을 실행하면 아래와 같은 SQL문이 실행된다.
select t.*
from Orders o
innser join Member m on o.member_id=m.id
inner join Team t on m.team_id =t.id
inner join Product p on o.product_id = p.id
총 3번의 조인이 발생한 것을 알 수 있다.
연관 경로 탐색을 통해 원하는 값을 조회하기가 훨씬 수월해졌으니, 잘 활용하면 좋을 듯 하다.
2. 컬렉션 값 연관 경로 탐색
select t.members.username from Team t
위의 예시에서 오류를 찾았는가?
흔히 저지르는 실수인데, 컬렉션 값에서 경로 탐색하였다.
select t.members from Team t
이렇게 해야 한다.
만약 컬렉션에서 경로 탐색을 하고 싶으면 직접적인 조인을 사용해서 새로운 별칭을 얻어 사용해야 한다.
select m.username from Team t join t.members m
** 경로 탐색을 사용한 묵시적 조인 시 주의사항 정리
1. 항상 내부 조인임을 인식하기
2. 컬렉션은 경로 탐색의 끝! 하고 싶으면 명시적 조인을 통해 별칭 얻기
JPQL의 서브쿼리는 WHERE, HAVING 절에서만 사용할 수 있고, SELECT , FROM 절에서는 사용할 수 없다.
select m from Member m
where m.age > (select avg(m2.age) from Member m2)
나이가 평균보다 많은 회원을 찾는 쿼리이다.
서브쿼리 함수는 SQL과 비슷하니 빠르게 살펴보고 넘어가겠다.
혹시 이해가 가지 않는 부분이 있다면, SQL을 먼저 살펴보고 오는 것을 추천드린다.
SELECT m
FROM Member m
WHERE m.team = ANY (SELECT t FROM Team t)
// 어떤 팀이든 팀에 소속된 회원을 조회
SELECT m
FROM Member m
WHERE m.team = ALL (SELECT t FROM Team t)
//ALL로 한다면, 모든 팀에 소속된 회원은 없으니 어떤 회원도 조회되지 않음
종류 | 설명 | 예제 |
---|---|---|
문자 | 작은따옴표 사이에 표현 | 'HELLO' |
숫자 | L, D, F | 10L, 10D, 10F |
날짜 | DATE(d 'yyyy-mm-dd'} , TIME(t 'hh-mm-ss'), DATETIME(ts 'yyyy-mm'dd hh:mm:ss.f'} | {d '2001'-'12'-'24}, {t '10-11-11'}, {ts '2001-12-25 10-11-11.123'} |
Boolean | TRUE, FALSE | |
Enum | 패키지명을 포함한 전체 이름을 사용해야 함 | jpabook.MemberType.Admin |
엔티티 타입 | 주로 상속과 관련해서 사용 | TYPE(m)=Member |
사용 예시는 SQL과 유사하니 생략하겠다.
여기서 NULL을 비교하고 싶을땐 =null을 하면 안되고, 반드시 IS NULL을 사용해야 한다.
그리고 컬렉션에서만 사용하는 컬렉션 식이 있는데, 컬렉션은 컬렉션 식 이외에 다른 식은 사용할 수 없다.
빈 컬렉션 비교 식
{컬렉션 값 연관 경로} IS [NOT] EMPTY
:컬렉션에 값 이 비어있으면 참
select m from Member m where m.orders is null
->이렇게 하면 컬렉션에 컬렉션식을 사용하지 않았으니 오류가 난다.
select m from Member m where m.orders is empty
->이렇게 사용해야 한다.
컬렉션의 멤버 식
{엔티티나 값} [NOT] MEMBER [OF] {컬렉션 값 연관 경로}
:엔티티나 값이 컬렉션에 포함되어 있으면 참
예시: select t from Team t
where :memberParam member of t.members
스칼라 식
스칼라 식에는 수학 식 (+,0,*,/ 등) 뿐만 아니라
문자 함수 (CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE (=검색 위치부터 문자를 검색함, 못 찾으면 0을 반환) ),
수학 함수 (ABS , SQRT, MOD, SIZE, INDEX (= LIST 타입 컬렉션의 위치값을 구함 -> t.members m where INDEX(m) > 3 처럼)) ,
날짜 함수 (CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP)를 사용할 수 있다.
이때 날짜 함수의 경우 데이터베이스들은 각자의 방식으로 날짜 함수를 지원하는데,
오라클 방언을 사용하면 to_dte, to_char 함수를 사용할 수 있다. 하지만 다른 데이터베이스를 사용하면 동작하지 않는다.
함수들의 사용예시는 SQL에서 사용하는 것과 크게 다르지 않아 생략하였다.
CASE 식
select
case when m.age <= 10 then '학생요금'
when m.age >=60 then '경로요금'
else '일반요금'
end
from Member m
select case t.name
when 'A' then '인센티브110%'
when 'B' then '인센티브120%'
else '인센티브 105%'
end
from Team t
select coalesce(m.username, '이름 없는 회원')from Member m
// m.username 이 null이면 '이름 없는 회원'이 반환
select NULLIF(m.username ,'관리자')from Member m
// m.username 이 관리자이면 null을 반환하고, 나머지는 본인의 이름을 반환
JPQL로 부모 엔티티를 조회하면 자식 엔티티도 함께 조회된다.
이때 TYPE를 이용하면 조회 대상을 특정 자식 타입으로 한정할 수 있다.
select i from Item i where type(i) IN (Book, Movie)
위와 같이 하면 Item 중에 Book 과 Movie만 조회된다.
TREAT
JPA 2.1에 추가된 기능인데, 자바 타입 캐스팅과 유사하다.
상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.
select i from Item where treat(i as Book).author = 'kim'
부모 타입 Item을 자식 타입 Book으로 다루어, 자식에 있는 author 필드에 접근이 가능하다.
JPA 2.1부터 지원된다.
문법 : function_invocation :: = FUNCTION(fucntion_name . {function-arg}*)
예시: select function ('group_concat', i.name) from Item i
하이버네이트 구현체를 사용하려면 방언 클래스를 상속해서 구현하고, 사용할 데이터베이스 함수를 미리 등록해야 한다.
public class MyH2Dialect extends H2Dialect {
public MyH2Dialect () {
registerFunction ("group_concat", new StandardSQLFunction ("group_concat", StandardBasicTypes.String));
}
}
그리고 등록해야 한다.
<property name = "hibernate.dialect" value="hello.MyH2Dialect">
기본 키 값
JPQL에서 엔티티 객체 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
select count(m) from Member m
위의 경우 select count(m.id) as cnt from Member m 과 같은 쿼리가 생성된다.
자동적으로 엔티티의 기본 키 값이 사용된 것을 볼 수 있다.
또한 엔티티를 직접 파라미터로 받을 수도 있는데,
em.createQuery("select m from Member m where m =:member")
.setParameter("member",member).getResultList();
위를 실행하면 마찬가지로 엔티티의 기본 키 값이 사용된다.
(실행되는 쿼리 -> select m.* from Member m where m.id = ? )
외래 키 값
외래 키 또한 유사하게 엔티티로 대체할 수 있다.
Team team = em.find(Team.class , 1L);
String query = "select m from Member m where m.team = :team";
em.createQuery(query)
.setParameter("team", team)
.gerResultList();
엔티티를 넣었지만, 외래 키를 이용하여 m.team_id 가 1L인 회원들이 조회가 되게 된다.
JPQL은 동적 쿼리와 정적 쿼리로 나눌 수 있다.
동적 쿼리 : JPQL을 문자로 완성해서 직접 넘기는 것을 말함. 런타임에 특정 조건에 따라 동적으로 구성할 수 있음.
정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용함 (=Named 쿼리). 한 번 정의하면 변경할 수 없음.
Named는 애플리케이션 로딩 시점에 문법을 체크하고 미리 파싱하여, 오류를 빨리 확인할 수도 있고 사용할 때는 파싱된 결과를 재사용하여 성능상 이점도 있다.
Named 쿼리는 @NamedQuery 어노테이션을 사용해서 자바 코드에 작성하거나 XML 문서에 작성할 수 있다.
✏️ 어노테이션에 정의하여 사용하기
@Entity
@NamedQuery (
name = "Member.findByUsername",
query ="select m from Member m where m.username = :username")
public class Member {
...
}
//이렇게 쿼리를 정의하고,
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("usernae","회원1")
.getResultList();
//이렇게 사용한다
엔티티에 2개 이상의 Named 쿼리를 정의하려면, @NamedQueries를 이용하면 된다.
✏️ XML에 정의하여 사용하기
멀티라인 문자를 자바 언어로 쓰는 것은 조금 수고스러운 일이다.
XML을 사용하면 그래도 조금 더 간편하게 쓸 수 있다.
<entity-mappings xmlns="https://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
<named-query name="Member.findByUsername">
<qeury><CDATA[
select m
from Member m
where m.usernmae = :username
]></query>
</named-query>
</entity-mappings>
정의한 xml을 인식하도록 META-INF/persistence.xml에 넣어주는 것도 잊지 말자
<persistence-unit name = "jpabook">
<mapping-file>META-INF/ormMember.xml</mapping-file>
...
참고로 XML과 어느테이션에 같은 설정이 있으면 XML 이 우선권을 가진다.
후 많기도 많다.
이어서, 다음 장에서 QueryDSL 부터 알아보겠다.
참조 : 자바 ORM 표준 JPA 프로그래밍 : 김영한