10 장에서 다루는 내용입니다.

  • 객체지향 쿼리 소개
  • Criteria
  • QueryDSL
  • Native SQL
  • 객체지향 쿼리 심화

10.1 객체지향 쿼리 소개

EntityMangager.find() 메서드를 사용하면 식별자로 엔티티 하나를 조회하고, 조회한 엔티티에 객체 그래프 탐색을 사용해서 연관된 엔티티를 찾을 수 있습니다.

  • 식별자로 조회 : EntityMangager.find()
  • 객체 그래프 탐색 : a.getB().getC()

만약 30살 이상인 회원을 모두 검색하고 싶으면 ? 모든 엔티티를 메모리에 올려두고 검색하는 것은 현실적이지 않습니다. 결국 데이터는 DB에 있으므로 SQL 로 최대한 걸러야합니다. 하지만 ORM 을 사용하면 DB table 이 아닌, 엔티티 객체를 대상으로 검색하므로 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요합니다. 그래서 만들어진 것이 JPQL 입니다.

다음은, 검색 방법으로 JPA 가 공식 지원하는 기능입니다.

  • JPQL (Java Persistence Query Language)
  • Criteria Query
  • Native SQL

다음은, JPA 가 공식 지원하는 기능은 아니지만 알아둘 가치가 있습니다.

  • QueryDSL
  • JDBC 직접 사용 / MyBatis 같은 SQL Mapper 프레임워크 사용
10.1.1 JPQL 소개
1
2
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
10.1.2 Criteria Query 소개

JPQL을 생성하는 빌더 클래스입니다. 문자가 아닌, query.select(m).where(…) 처럼 프로그래밍 코드로 JPQL을 작성할 수 있습니다.

장점은 다음과 같습니다.

  • 컴파일 시점에 오류 발견
  • IDE를 사용하면 코드 자동 완성 지원
  • 동적 쿼리 작성 편이

하지만, 복잡하고 장황해서 Criteria 로 작성한 코드가 한눈에 들어오지 않는 단점이 있습니다.

10.1.3 QueryDSL 소개

Criteria 처럼 JPQL 빌더 역할을 합니다. Criteria 에 비해, 작성한 코드가 한눈에 들어오고 단순하고 사용하기 쉽습니다.

JPA 표준이 아니고 오픈 소스 프로젝트입니다. JPA 뿐만 아니라, JDO, MongoDB, Java Collection, Lucene, Hibernate Search 도 거의 같은 문법으로 지원합니다.

10.1.4 Native SQL

SQL 을 직접 사용하는 기능입니다. 그래서, 데이터베이스를 변경하면 네이티브 SQL 로 수정해야합니다.

10.1.5 JDBC 직접 사용 / MyBatis 같은 SQL Mapper 프레임워크 사용

JDBC connection 에 직접 접근하고 싶으면, JPA는 JDBC connection 을 획득하는 API 를 제공하지 않으므로, JPA 구현체가 제공하는 방법을 사용해야 합니다.

10.2 JPQL

1절에서 엔티티를 쿼리하는 다양한 방법을 소개했지만, 어떤 방법을 사용하든 JPQL 에서 모든 것이 시작합니다. 다음은 JPQL의 특징입니다.

  • 객체지향 쿼리 언어입니다. 엔티티 객체를 대상으로 쿼리합니다.
  • 특정 데이터베이스에 SQL 에 의존하지 않습니다.
  • JPQL 도 결국 SQL 로 변환됩니다.
10.2.1 기본 문법과 쿼리 API

EntityManger.persist() 메소드를 사용하면 되기 때문에 INSERT 문은 없습니다.

SELECT 문
1
SELECT m FROM Member AS m where m.username = 'Hello'
TypeQuery, Query

반환할 타입을 명확히 지정할 수 있으면 TypeQuery, 반환할 타입을 명확히 지정할 수 없으면 Query

1
2
3
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member AS m", Member.class);

Query query = em.createQuery("SELECT m.username¸ m.age FROM Member AS m");
결과 조회

다음 메소드를 호출하면 실제 쿼리를 실행해서 데이터베이스를 조회합니다.

  • query.getResultList()
  • query.getSingleResult()
10.2.2 파라미터 바인딩
이름 기준 파리미터

위치 기준 파라미터 바인딩 방식보다 명확합니다.

1
2
3
4
5
String usernameparam = "user1";

TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class);

query.setParameter("username", usernameparam);
위치 기준 파라미터
1
2
3
4
5
6
String usernameparam = "user1";

List<Member> members = em
.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class)
.setParameter(1, usernameParam)
.getResultList();
10.2.3 프로젝션

SELECT 절에 조회할 대상을 지정하는 것입니다.

  • 엔티티 프로젝션
    • 조회한 엔티티는 영속성 컨텍스트에서 관리됩니다.
  • 임베디드 타입 프로젝션
    • 임베디드 타입은 값 타입입니다. 따라서, 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않습니다.
  • 스칼라 타입 프로젝션
    • 기본 데이터타입입니다.
    • SELECT username…. / SELECT AVG(o.orderAmout)…
  • 여러 값 조회
    • 꼭 필요한 데이터들만 조회하는 경우입니다.
  • New 명령어
    • 반환 받을 클래스를 지정하여 이 클래스의 생성장에 JPQL 조회 결과를 넘길 수 있습니다.
    • TypeQuery 를 사용할 수 있어서 지루한 객체 변환 작업을 줄일 수 있습니다.
10.2.4 페이징 API

페지징을 다음 두 API 로 추상화했습니다.

  • setFirstResult(int startPosition)
    • 조회 시작 위치
  • setMaxResults(int maxResult)
    • 조회할 데이터 수
10.2.5 집합과 정렬

집합은 통계 정보를 구할 때 사용합니다.

10.2.6 JPQL 조인

SQL 조인과 기능은 같고 문법만 약간 다릅니다.

내부 조인
1
SELECT m FROM Member m INNER JOIN m.team t WHERE t.name = :teamName
외부 조인
1
SELECT m FROM Member m LEF [OUTER] JOIN m.team t
컬렉션 조인

일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것입니다.

세타 조인

전혀 관계 없는 엔티티도 조회 할 수 있습니다.

1
2
3
select count(m) 
from Member m, Team t
where m.username = t.name
JOIN ON 절 (JPA 2.1)

조인 대상을 필터링하고 조인할 수 있습니다.

1
2
3
select m, t 
from Member m
left join m.team t on t.name = 'A'
10.2.7 페치 조인

연관된 엔티티나 컬렉션을 한 번에 같이 조회합니다.

엔티티 페치 조인
1
2
select m
from Member m join fetch m.team
컬렉션 페치 조인
1
2
3
select t
from Team t join fetch t.members
where t.name = '팀A'
페치 조인과 DISTINCT
1
2
3
select distint t
from Team t join fetch t.members
where t.name = '팀A'
페치 조인과 일반 조인의 차이

다음은, 팀만 조회하고 조인했던 회원은 전혀 조회하지 않습니다.

1
2
3
select t
from Team t join t.members m
where t.name = '팀A'

JPQL은 결과를 반환할 때 연관관계까지 고려하지 않습니다. 단지 SELECT 절에 지정한 엔티티만 조회합니다.

페치 조인의 특징과 한계
  • 페치 조인 대상에는 별칠을 줄 수 없습니다.
  • 둘 이상의 컬렉션을 페치할 수 없습니다.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없습니다.
10.2.8 경로 표현식

. 을 찍어서 객체 그래프를 탐색하는 것입니다.

  • 상태 필드
    • 단순히 값을 저장하기 위한 필드
  • 연관 필드
    • 객체 사이의 연관 관계를 위한 필드, 임베디드 타입 포함
경로 표현식과 특징
  • 상태 필드 경로
    • 경로 탐색의 끝
  • 단일 값 연관 경로
    • 묵시적으로 내부조인
    • 계속 탐색 가능
  • 컬렉션 값 연관 경로
    • 묵시적으로 내부조인
    • 더는 탐색 불가능 (단, FROM 절에서 조인 통해 별칭 얻으면 별칭으로 탐색 가능)
10.2.9 서브 쿼리

WHERE, HAVING 절에서만 사용 가능합니다.

서브 쿼리 함수
  • EXISTS
  • ALL|ANY|SOME
  • IN
10.2.10 조건식
10.2.11 다형성 쿼리

부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회합니다.

10.2.12 사용자 정의 함수 호출 (JPA 2.1)
10.2.13 기타 정리
10.2.14 엔티티 직접 사용

#####10.2.15 Named Query : 정적 쿼리

미리 정의한 쿼리에 이름을 부여해서 필요할 때마다 사용하는 정적인 쿼리입니다.

10.3 Criteria

Criteria Query 는 JPQL 을 자바 코드로 작성하도록 도와주는 빌더 클래스 API 입니다.

10.4 QueryDSL

쿼리를 문자가 아닌 코드로 작성해도 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는 프로젝트입니다. JPA Criteria 를 대체할 수 있습니다.

10.4.1 설정
10.4.2 시작
1
2
3
4
5
6
7
8
9
10
11
12
public void queryDSL(){

EntityManager em = emf.createEntityManager();

JPAQuery query = new JPAQuery(em);
QMember qMember = new QMember("m"); //생성되는 JPQL의 별칭이 m
List<Member> members =
query.from(qMember)
.where(qMember.name.eq("회원1"))
.orderBy(qMember.name.desc())
.list(qMember);
}
기본 Q 생성

쿼리 타입(Q) 은 사용하기 편리하도록 기본 인스턴스를 보관하고 있습니다. 하지만, 같은 엔티티를 조인하거나 같은 엔티티를 서브쿼리에 사용하면 같은 별칭이 사용되므로, 별칭을 직접 지정해야합니다.

1
2
3
4
public class QMember extends EntityPathBase<Member> {
public static final QMember member = new QMember("member1");
...
}

쿼리 타입은 다음과 같이 사용합니다.

1
2
QMember qMember = new QMember("m"); //직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
10.4.3 검색 조건 쿼리
1
2
3
4
5
JPAQuery query = new JPAQuery(em);
QItem item = QItem.item;
List<Item> list = query.from(item)
.where(item.name.eq("goodProduct").and(item.price.gt(20000)))
.list(item); //조회할 프로젝션 지정

#####10.4.4 결과 조회

쿼리 작성 후에, 결과 조회 메소드를 호출하면 실제 데이터베이스를 조회합니다. 대표적인 결과 조회 메소드는 다음과 같습니다.

  • uniqueResult()
    • 조회 결과가 한건일 때 사용
  • singleResult()
    • uniqueResult() 와 결과가 같지만, 결과가 하나 이상이면 처음 데이터를 반환
  • list()
    • 조회 결과가 하나 이상일 때 사용

#####10.4.5 페이징과 정렬

1
2
3
4
5
6
7
QItem item = QItem.item;

query.from(item)
.where(item.price.gt(20000))
.orderBy(item.price.desc(), item.stockQuantity.asc())
.offset(10).limit(20)
.list(item)

#####10.4.6 그룹

1
2
3
4
query.from(item)
.groupBy(item.price)
.having(item.price.gt(1000))
.list(item)

#####10.4.7 조인

1
2
3
4
5
6
7
8
QOrder order = QOrder.order;
QMember member = QMember.member;
QOrderItem orderItem = QOrderItem.orderItem;

query.from(order)
.join(order.member, member)
.leftJoin(order.orderItems, orderItem)
.list(order);

#####10.4.8 서브 쿼리

1
2
3
4
5
6
7
8
QItem item = QItem.item;
QItem itemSub = new QItem("itemSub");

query.from(item)
.where(item.price.eq(
new JPASubQuery().from(itemSub).unique(itemSub.price.max())
))
.list(item)

#####10.4.9 프로젝션과 결과 반환

프로젝션 대상이 하나

프로젝션 대상이 하나면 해당 타입으로 반환합니다.

여러 칼럼 반환과 튜플

프로젝션 대상으로 여러 필드를 선택하면 QueryDSL 은 기본으로 com.mysema.query.Tuple 이라는 Map 과 비슷한 내부 타입을 사용합니다.

1
2
3
4
5
6
7
8
9
QItem item = QItem.item;

List<Tuple> result = query.from(item).list(item.name, item.price);
// List<Tuple> result = query.from(item).list(new QTuple(item.name, item.price)); 와 같음

for(Tuple tuple : result){
System.out.println("name = " + tuple.get(item.name));
System.out.println("name = " + tuple.get(item.price));
}
빈 생성

쿼리 결과를 엔티니가 아닌 특정 객체로 받고 싶으면 빈 생성 기능을 사용합니다. 객체를 생성하는 방법은 다음과 같은 것들이 있습니다.

  • 프로퍼티 접근
  • 필드 직접 접근
  • 생성자 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ItemDTO{

private String username;
private int price;

public ItemDTO() {}

public ItemDTO(String username, int price){
this.username = username;
this.price = price;
}

//GETTER, SETTER ..
}

// 프로퍼티 접근
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.bean(ItemDTO.class, item.name.as("username"), item.price));

//필드 직접 접근
List<ItemDTO> result = query.from(item).list(
Projections.fields(ItemDTO.class, item.name.as("username"), item.price));

//생성자 사용
List<ItemDTO> result = query.from(item).list(
Projections.constructor(ItemDTO.class, item.name, item.price));
DISTINCT
1
query.distinct().from(item)

#####10.4.10 수정, 삭제, 배치 쿼리

10.4.11 동적 쿼리

com.mysema.query.BooleanBuilder 를 사용하면 특정 조건에 따른 동적 쿼리를 편리하게 생성할 수 있습니다.

10.4.12 메소드 위임

쿼리 타입에 검색 조건을 직접 정의할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//검색 조건 정의
public class ItemExpression{

@QueryDelegate(Item.class) // 이 기능을 적용할 엔티티 지정
public statc BooleanExpression isExpensive(QItem item, Integer price){
return item.price.gt(price);
}
}

//쿼리 타입에 생성된 결과
public class QItem extends EntityPathBase<Item> {
...
public com.mysema.qeury.types.expr.BooleanExpression isExpensive(Integer price){
return ItemExpression.ixExpensieve(this, price);
}
}

//메소드 위임 기능 사용
query.from(item).where(item.isExpensive(30000)).list(item);

10.5 Native SQL

다양한 이유로 JPQL 을 사용할 수 없을 때 JPA 는 SQL 을 직접 사용할 수 있는 기능을 제공하는데, 이것을 Native SQL 이라고 합니다. 네이티브 SQL 을 사용하면 엔티티를 조회할 수 있고 JPA 가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있습니다. 반면에 JDBC API 를 직접 사용하면 단순히 데이터의 나열을 조회할 뿐입니다.

네이티브 SQL 도 JPQL 을 사용할 때와 마찬가지로 Query, TypeQuery(Named Native Query 의 경우) 를 반환합니다. 따라서 JPQL API 를 그대로 사용할 수 있습니다.

10.5.1 Native SQL 사용

네이티브 쿼리 API 는 세 가지 입니다.

  • 결과 타입 정의
1
public Query createNativeQuery(String sqlString, Class resultClass);
  • 결과 타임 정의 할 수 없을 때
1
public Query createNativeQuery(String sqlString);
  • 결과 매핑 사용
1
public Query createNativeQuery(String sqlString, String resultSetMapping);
10.5.5 스토어드 프로시저

JPA 2.1 부터 지원합니다.

10.6 객체 지향 쿼리 심화

10.6.1 벌크 연산

여러 건을 한번에 수정하거나 삭제할 때 벌크 연산을 사용합니다. 벌크 연산을 사용할 때는 벌크 연산이 영속성 컨텍스트를 무시하고 데이터이스에 직접 쿼리합니다.

10.6.2 영속성 컨텍스트와 JPQL
  • JPQL은 항상 데이터베이스를 조회합니다.
  • JPQL로 조회한 엔티티는 영속 상태입니다.
  • 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환합니다.
10.6.3 JPQL 과 플러쉬 모드