[자바 표준 ORM JPA]10장 객체지향 쿼리 언어 - 2
QueryDSL
JPQL을 작성하므로 문법 오류를 컴파일 단계에서 잡을 수 있고 자동완성 기능의 도움을 받을 수 있는 등 여러 가지 장점이 있지만 너무 복잡하고 어렵다. QueryDSL도 JPQL 빌더 역할을 하므로 대체할 수 있다.
메이븐 설정을 마치고 콘솔에서 mvn compile
을 입력하면 outputDirectory
에 지정한 target/generated-sources 위치에 Q로 시작하는 쿼리 타입들이 생성된다.
시작
public void queryDSL() {
EntityManager em = emf.crateEntityManager();
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);
}
- QueryDSL을 사용하려면 우선 JPAQuery 객체를 생성해야 하는데 이때 엔티티 매니저를 생성자에 넘겨준다.
- 다음으로 사용할 쿼리 타입을 생성하는데 생성자에는 별칭을 주면 된다. 이 별칭을 JPQL에서 별칭으로 사용한다.
기본 Q 생성
쿼리 타입은 사용하기 편하도록 기본 인스턴스를 보관하고 있다.
@Generated("com.querydsl.codegen.EntitySerializer")
public class QAccount extends EntityPathBase<Account> {
public static final QAccount account = new QAccount("account");
...
하지만 같은 엔티티끼리 조인하거나 서브쿼리에 사용하면 같은 별칭이 되므로 이때는 별칭을 직접 지정해서 사용해야 한다.
프로젝션과 결과 반환
select 절에 조회 대상을 지정하는 것을 프로젝션이라 했다.
프로젝션 대상이 하나
QItem item = QItem.item;
List<String> result = query.from(item).list(item.name);
프로젝션 대상이 하나면 해당 타입으로 반환한다.
여러 컬럼 반환과 튜플
프로젝션 대상으로 여러 필드를 선택하면 QueryDSL은 기본으로 Tuple이라는 Map과 비슷한 내부 타입을 사용한다.
조회 결과는 tuple.get()
메서드에 조회한 쿼리 타입을 지정하면 된다.
QItem item = QItem.item;
List<Tuple> result = query.from(item).list(item.name, item.price);
for (Tuple tuple : result) {
System.out.println("name = " + tuple.get(item.name));
System.out.println("price = " + tuple.get(item.price));
}
빈 생성(DTO)
쿼리 결과를 엔티티가 아닌 특정 객체로 받고 싶으면 빈 생성 기능을 사용한다. (더 자주 사용됨) QueryDSL은 객체를 생성하는 다양한 방법을 제공한다.
- 프로퍼티 접근
- 필드 직접 접근
- 생성자 사용
원하는 방법을 지정하기 위해 Projections를 사용하면 된다.
예제 ItemDTO
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ItemDTO {
private String username;
private int price;
}
프로퍼티 접근(Setter)
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.bean(ItemDTO.class, item.name.as("username"), item.price));
bean()
메서드는 Setter를 사용해서 값을 채운다. as
를 사용해서 쿼리 결과인 name을 ItemDTO가 가지고 있는 프로퍼티인
username으로 변경했다. 이처럼 쿼리 결과와 매핑할 프로퍼티 이름이 다르면 as
를 사용해서 별칭을 주면 된다.
필드 직접 접근
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.fields(ItemDTO.class, item.name.as("username"), item.price));
fields()
메서드는 필드에 직접 접근해서 값을 채워준다. 필드를 private
로 설정해도 동작한다.
생성자 사용*
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.constructor(ItemDTO.class, item.name, item.price));
지정한 프로젝션과 파라미터 순서가 같은 생성자가 필요하다.
DISTINCT
query.distinct().from(item)....
수정, 삭제 배치 쿼리
QueryDSL도 수정, 삭제같은 배치 쿼리를 지원한다. JPQL 배치 쿼리와 같이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점을 유의하자.
수정 배치 쿼리*
QItem item = QItem.item;
JPAUpdateClause updateClause = new JPAUpdateClause(em, item);
long count = updateClause.where(item.name.eq("시골 개발자의 JPA 책"))
.set(item.price, item.price.add(100))
.execute();
삭제 배치 쿼리
QItem item = QItem.item;
JPADeleteClause deleteClause = new JPADeleteClause(em, item);
long count = deleteClause.where(item.name.eq("시골개발자의 JPA 책"))
.execute()
동적 쿼리
BooleanBuilder를 사용하면 특정 조건에 따른 동적 쿼리를 편리하게 생성할 수 있다.
SearchParam param = new SearchParam();
param.setName("시골개발자");
param.setPrice(10000);
QItem item = QItem.item;
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(param.getName())) {
builder.and(item.name.contains(param.getName()));
}
if (param.getPrice() != null) {
builder.and(item.price.gt(param.getPrice()));
}
List<Item> result = query.from(item)
.where(builder)
.list(item);
메서드 위임
메서드 위임Delegate Methods 기능을 사용하면 쿼리 타입에 검색 조건을 직접 정의할 수 있다.
검색 조건 정의
public class ItemExpression {
@QueryDelegate(Item.class)
public static BooleanExpression isExpensive(QItem item, Integer price) {
return item.price.gt(price);
}
}
메서드 위임 기능을 사용하기 위해 우선 static 메서드를 만들고 QueryDelegate 애노테이션으로 이 기능을 적용할 엔티티를 지정한다. 메서드의 첫 번째 파라미터에는 대상 엔티티의 쿼리 타입을 지정하고 나머지는 필요한 파라미터를 정의한다.
쿼리 타입에 생성된 결과
public class QItem extends EntityPathBase<Item> {
...
public com.mysema.query.types.expr.BooleanExpression isExpensive(Integer price) {
return ItemExpression.isExpensive(this, price);
}
}
이제 메서드 위임 기능을 사용해보자.
query.from(item).where(item.isExpensive(30000)).list(item);
필요하다면 String, Date 같은 자바 기본 내장 타입에도 메서드 위임 기능을 사용할 수 있다.
네이티브 SQL
JPQL은 표준 SQL이 지원하는 대부분의 문법과 SQL 함수들을 지원하지만 특정 데이터베이스 종속적 기능은 지원하지 않는다.
JPQL에서 특정 데이터베이스에 종속적인 기능을 지원하는 방법은 다음과 같다.
- 특정 데이터베이스만 사용하는 함수
- JPQL에서 네이티브 SQL 함수를 호출할 수 있다 JPA 2.1.
- 하이버네이트는 데이터베이스 방언에 각 데이터베이스에 종속적인 함수들을 정의해두었다.
- 특정 데이터베이스만 지원하는 SQL 쿼리 힌트
- 하이버네이트를 포함한 몇몇 JPA 구현체들이 지원한다.
- 인라인 뷰, UNION, INTERSECT
- 하이버네이트는 지원하지 않지만 일부 JPA 구현체들이 지원한다.
- 스토어드 프로시저
- JPQL에서 스토어드 프로시저를 호출할 수 있다 JPA 2.1
JDBC API와의 차이점이라면 네이티브 SQL은 JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있다.
네이티브 쿼리 API는 다음 3가지가 있다.
-
결과 타입 정의
public Query createNativeQuery(String sqlString, Class resultClass);
-
결과 타입을 정의할 수 없을 때
public Query createNativeQuery(String sqlString);
-
결과 매핑 사용
public Query createNativeQuery(String sqlString, String resultSetMapping);
네이티브 SQL도 JPQL을 사용할 때와 마찬가지로 Named 쿼리를 사용할 때 Query.TypeQuery를 반환한다. 따라서 JPQL API를 그대로 사용할 수 있다.
10.6 객체지향 쿼리 심화
10.6.1 벌크 연산
수백개 이상의 엔티티를 하나씩 처리하기에는 시간이 너무 오래걸린다. 이럴 때 여러 건을 한 번에 수정하거나 삭제하는 연산이다.
UPDATE 벌크 연산
String qlString =
"update Product p " +
"set p.price = p.price * 1.1 " +
"where p.stickAmount < : stockAmount";
int resultCount = em.createQuery(qlString)
.setParameter("stockAmount", 10)
.executeUpdate();
DELETE 벌크 연산
String qlString =
"delete from Product p" +
"where p.price < :price";
int resultCount = em.createQuery(qlString)
.setParameter("price", 100)
.executeUpdate();
JPA 표준은 아니지만 하이버네이트는 INSERT 벌크 연산도 지원한다. 다음 코드는 100원 미만의 모든 상품을 ProductTemp에 저장한다.
INSERT 벌크 연산
String qlString =
"insert into ProductTemp(id, name, price, stockAmount) " +
"select p.id, p.name, p.price, p.stockAmount from Product p " +
"where p.price < :price";
int resultCount = em.createQuery(qlString)
.setParameter("price", 100)
.executeUpdate();
벌크 연산의 주의점
벌크 연산을 사용할 때는 벌크 연산이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점에 주의해야 한다.
- 가격이 1000원인 상품A를 조회했다. 조회된 상품 A는 영속성 컨텍스트에서 관리된다.
- 벌크 연산으로 모든 상품의 가격을 10% 상승시켰다. 따라서 가격은 1100원이 되어야 한다.
- 벌크 연산을 수행한 후에 가격을 출력하면 1100원이 아니라 1000원이 출력된다.
벌크 연산은 영속성 컨텍스트를 통하지 않고 데이터베이스에 직접 쿼리하므로 영속성 컨텍스트에 있는 상품 A와 데이터베이스에 있는 상품A의 가격이 다를 수 있다. 이런 문제를 해결하는 방법을 살펴보자.
-
em.refresh() 사용
벌크 연산을 수행한 직후에 정확한 상품 A 엔티티를 사용해야 한다면
em.refresh(productA)
로 다시 조회하면 된다. -
벌크 연산 먼저 실행
벌크 연산을 먼저 실행하고 나서 상품 A를 조회하면 이미 변경된 상품 A를 조회하게 된다.
-
벌크 연산 수행 후 영속성 컨텍스트 초기화
영속성 컨텍스트를 초기화하면 이후 엔티티를 조회할 때 벌크 연산이 적용된 데이터베이스에서 엔티티를 조회한다.
가능하면 벌크 연산을 가장 먼저 실행하는 것이 좋고 상황에따라 초기화하는 것도 필요하다.
find() vs JPQL
em.find() 메서드는 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾는다. 해당 엔티티가 영속성 컨텍스트에 있으면 메모리에서 바로 찾으므로1차 캐시 성능상 이점이 있다.
그에 비해 JPQL은 항상 데이터베이스에 SQL을 실행해서 결과를 조회한다.
JPQL의 특징을 정리하자면
- JPQL은 항상 데이터베이스를 조회한다.
- JPQL로 조회한 엔티티는 영속 상태다.
- 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환한다.
10.6.3 JPQL과 플러시 모드
플러시는 영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화하는 것이다. JPA는 플러시가 일어날 때 영속성 컨텍스트에 등록, 수정, 삭제한 엔티티를 찾아서 INSERT, UPDATE, DELETE SQL을 만들어 데이터베이스에 반영한다.
플러시를 호출하려면 em.flush()
를 직접 사용할수도 있지만 보통 플러시 모드에 따라 커밋하기 직전이나
쿼리 실행 직전에 자동으로 호출된다. 플러시 모드는 FlushModeType.AUTO가 기본값이므로 JPA는 트랙젝션 커밋 직전이나
쿼리 실행 직전에 자동으로 플러시를 호출한다.
플러시 모드 설정
em.setFlushMode(FlushModeType.COMMIT); // 커밋 시에만 플러시
// 가격을 1000 -> 2000으로 변경
product.setPrice(2000);
// 1.em.flush() 직접 호출
// 가격이 2000인 상품 조회
Product product2 =
em.createQuery("select p from Product p where p.price = 2000", Product.class)
.setFlushMode(FlushModeType.AUTO) // 2. setFlushMode()
.getSingleResult();
JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 데이터베이스에서 데이터를 조회한다. 따라서 JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해야 한다.
플러시 모드의 기본값은 AUTO이므로 일반적인 상황에서는 위 내용을 고려하지 않아도 된다. 그렇다면 왜 COMMIT 모드를 사용하는 것일까?
플러시 모드와 최적화
COMMIT 모드는 트랜잭션을 커밋할 때만 플러시하고 쿼리를 실행할 때는 플러시하지 않는다. 따라서 데이터 무결성에 심각한 피해를 줄 수 있는데, 그럼에도 플러시가 너무 자주 일어나는 상황에 이 모드를 사용하면 플러시 횟수를 줄여서 성능을 최적화할 수 있다.
Leave a comment