QueryDSL을 사용하려면 com.mysema.query.jpa.impl.JPAQuery 객체를 생성해야 한다.
이때 엔티티 매니저를 생성자에게 넘겨준다.
다음으로 사용할 쿼리 타입을 생성하는데 생성자에는 별칭을 주면 된다.
이 별칭을 JPQL에서 별칭으로 사용한다.
기본 Q 생성
쿼리 타입은 사용하기 편리하도록 기본 인스턴스를 보관하고 있다.
하지만 같은 엔티티를 조인하거나 같은 엔티티를 서브쿼리에 사용하면 같은 별칭이 사용되므로 이때는 별칭을 직접 지정해야 한다.
// Member 쿼리 타입
public class QMember extends EntityPathBase<Member> {
public static final QMember member new QMember("member1");
...
}
// 쿼리 타입 사용
QMember qMember = new QMember("m"); // 직접 지정
QMember qMember = QMember.member; // 기본 인스턴스 사용
// import static 활용
import static jpabook.jpashop.domain.QMember.member; // 기본 인스턴스
public void basic() {
EntityManager em = emf.createEntityManager();
JPAQuery query = new JPAQuery(em);
List<Member> members = query.from(member)
.where(member.name.eq("회원1"))
.orderBy(member.name.desc())
.list(member);
}
3. 검색 조건 쿼리
// 기본 쿼리 기능
JPAQuery query = new JPAQuery(em);
QItem item = QItem.item;
List<Item> list = query.from(item)
.where(item.name.eq("좋은상품").and(item.price.gt(20000)))
.list(item); // 조회할 프로젝션 지정
select item
from Item item
where item.name = ?1 and item.price >?2
QueryDSL의 where 절에는 and나 or가 사용 가능하다.
.where(item.name.eq("좋은상품"), item.price.gt(20000)) 이렇게도 사용가능한데 이때는 and 연산이 된다.
쿼리 타입의 필드는 필요한 대부분의 메소드를 명시적으로 제공한다.
item.price.between(10000,20000); // 가격이 10000~20000 상품
item.name.contains("상품1"); // 상품1이라는 이름을 포함한 상품
item.name.startWith("고급"); // 이름이 고급으로 시작하는 상품
4. 결과 조회
결과 조회 API는 com.mysema.query.Projectable에 정의되어 있다.
대표적인 결과 조회 메소드
uniqueResult() : 조회 결과가 한 건일 때 사용, 결과가 없으면 null, 하나이상이면 NonUniqueResultException 발생
singleResult() : uniqueResult()와 같지만 결과가 하나 이상이면 처음 데이터를 반환
정렬은 orderBy를 사용하며 쿼리 타입이 제공하는 asc(), desc()를 사용한다.
페이징은 offset과 limit을 적절히 조합해서 사용하면 된다.
// 페이징과 정렬 QueryModifiers 사용
QueryModifiers queryModifiers = new QueryModifiers(20L, 10L);
List<Item> list = query.from(item)
.restrict(queryModifiers)
.list(item);
// 실제 페이징 처리를 하려면 검색된 전체 데이터 수를 알아야 한다.
// 이때는 list() 대신에 아래와 같이 listResults()를 사용한다.
// 페이징과 정렬 listResult() 사용
SearchResults<Item> result = query.from(item)
.where(item.price.gt(10000))
.offset(10).limit(20
.listResults(item);
long total = result.getTotal(); // 검색된 전체 데이터 수
long limit = result.getLimit();
long offset = result.getOffset();
List<Item> results = result.getResults(); // 조회된 데이터
// listResult()를 사용하면 전체 데이터 조회를 위한 count 쿼리를 한 번 더 실행한다.
// 그리고 SearchResults를 반환하는데 이 객체에서 전체 데이터 수를 조회 할 수 있다.
6. 그룹
// groupBy() 사용
query.from(item)
.groupBy(item.price)
.having(item.price.gt(1000)) // 그룹화된 결과를 제한하려면 having을 사용
.list(item);
7. 조인
// 기본 조인
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);
// 조인 on 사용
query.from(order)
.leftJoin(order.orderItems, orderItem)
.on(orderItem.count.gt(2))
.list(order);
// 페치 조인 사용
query.from(order)
.innerJoin(order.member, member).fetch()
.leftJoin(order.orderItems, orderItem).fetch()
.list(order);
// from 절에 여러 조건 사용 (세타 조인)
query.from(order, member)
.where(order.member.eq(member))
.list(order);
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);
// 서브 쿼리 예제 - 여러건
QItem item = Qitem.item;
QItem itemSub = new QItem("itemSub");
query.from(item)
.where(item.in(
new JPASubQuery().from(itemSub)
.where(item.name.eq(itemSub.name))
.list(itemSub)
))
.list(item);
9. 프로젝션과 결과 반환
// 프로젝션 대상이 하나
QItem item = QItem.item;
List<String> resut = query.from(item).list(item.name);
for(String name : result) {
System.out.println("name = " + name);
}
// 예제 ItemDTO
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 ...
}
// 프로퍼티 접근(Setter)
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.bean(ItemDTO.class, item.name.as("username"), item.price));
Projections.bean() 메소드는 Setter를 사용해서 값을 채운다.
쿼리 결과는 name인데 ItemDTO는 username 프로퍼티를 가지고 있다.
이처럼 쿼리 결과와 매핑할 프로퍼티 이름이 다르면 as를 사용해서 별칭을 주면 된다.
// 필드 직접 접근
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.filed(ItemDTO.class, item.name.as("username"), item.price));
// Projections.fields() 메소드를 사용하면 필드에 직접 접근해서 값을 채워준다. private로 설정해도 동작
// 생성자 사용
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.constructor(ItemDTO.class, item.name, item.price));
// Projections.constructor() 메소드는 생성자를 사용, 지정한 프로젝션과 파라미터 순서가 같은 생성자가 필요
DISTINCT
query.distinct().from(item)... 처럼 사용한다.
10. 수정, 삭제 배치 쿼리
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(); // 상품의 가격을 100원 증가시킨다.
// 삭제 배치 쿼리
JPADeleteClause deleteClause = new JPADeleteClause(em, item);
long count = deleteClause.where(item.name.eq("시골개발자의 JPA 책"))
.execute(); // 이름이 같은 상품을 삭제한다.
11. 동적 쿼리
// 동적 쿼리 예제
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);
// 상품 이름과 가격 유무에 따라 동적 쿼리 생성
12. 메소드 위임
// 검색 조건 정의
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 BooleanExpression isExpensive(Integer price) {
isExpensive(Integer price) {
return ItemExpression.isExpensive(this, price);
}
}
}
// 메소드 위임 기능 사용
query.from(item).where(item.isExpensive(30000)).list(item);
// String, Date 같은 자바 기본 내장 타입에도 메소드 위임 기능을 사용할 수 있다.
@QueryDelegate(String.class)
public static BooleanExpression isHelloStart(StringPath stringPath) {
return stringPath.startsWith("Hello");
}
5. 네이티브 SQL
JPQL은 대부분의 문법과 SQL 함수들을 지원하지만 특정 데이터베이스에 종속적인 기능은 지원하지 못함
특정 데이터베이스만 지원하는 함수, 문법, 쿼리 힌트
인라인 뷰(From 절에서 사용하는 서브쿼리), UNION, INTERSECT
스토어드 프로시저
특정 데이터베이스만 사용하는 함수
JPQL에서 네이티브 SQL 함수를 호출할 수 있다.
하이버네이트는 데이터베이스 방언에 각 데이터베이스에 종속적인 함수들을 정의해 두었고 직접 호출할 함수를 정의할 수도 있다.
특정 데이터베이스만 지원하는 SQL 쿼리 힌트
하이버네이트를 포함한 몇몇 JPA 구현체들이 지원한다.
인라인 뷰(From 절에서 사용하는 서브쿼리), UNION, INTERSECT
하이버네이트는 지원하지 않지만 일부 JPA 구현체들이 지원한다.
스토어 프로시저
JPQL에서 스토어드 프로시저를 호출할 수 있다.
특정 데이터베이스만 지원하는 문법
오라클 CONNECT BY처럼 특정 데이터베이스에 종속적인 SQL 문법은 지원하지 않음
JPA가 지원하는 네이티브 SQL를 사용하는 이유는 엔티티를 조회할 수 있고 JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있기 때문이다.
1. 네이티브 SQL 사용
엔티티 조회
// 엔티티 조회 코드
String sql = "SLECT ID, AGE, NAME, TEAM_ID " +
"FROM MEMBER " +
"WHERE AGE > ?";
Query nativeQuery = em.createNativeQuery(sql, Member.class)
.setParameter(1, 20);
// 첫번째 파라미터는 네이티브 SQL을 입력하고 두번째 파라미터는 조회할 엔티티 클래스의 타입을 입력
// JPQL를 사용할 때와 거의 비슷하지만 실제 데이터베이스 SQL을 사용한다는 것과 위치기반 파라미터만 지원한다는 차이가 있다.
주의점 은 네이티브 SQL로 SQL만 직접 사용할 뿐이지 나머지는 JPQL을 사용할 떄와 동일하다.
조회한 엔티티도 영속성 컨텍스트에서 관리된다.
JPA는 공식적으로 네이티브 SQL에서 위치 기바 파라미터만 지원한다.
하이버네이트는 네이티브 SQL에 이름 기반 파라미터를 사용할 수 있다.
em.createNativeQuery()를 호출하면서 타입 정보를 주었는데도 TypeQuery가 아닌 Query가 리턴되는데 이것은 JPA 1.0에서 API 규약이 정의되어 버려 그렇다.
값 조회
// 값 조회
String sql = "SELECT ID, AGE, NAME, TEAM_ID " +
"FROM MEMBER " +
"WHERE AGE > ?";
Query nativeQuery = em.createNativeQuery(sql)
.setParameter(1, 10);
List<Object[]> resultList = nativeQuery.getResultList();
for(Object[] row : resultList) {
System.out.println("id = " + row[0]);
System.out.println("age = " + row[1]);
System.out.println("name = " + row[2]);
System.out.println("team_id = " + row[3]);
}
// em.createNativeQuery(SQL)의 두번쨰 파라미터를 사용하지 않으면
// JPA는 조회한 값들을 Object[]에 담아서 반환한다.
// 여기서는 스칼라 값들을 조회했을 뿐이므로 결과를 영속성 컨텍스트가 관리하지 않는다.
// JDBC로 데이터를 조회한 것과 비슷하다.
결과 매핑 사용
// 결과 매핑 사용
String sql = "SELECT M.ID, AGE, NAME, TEAM_ID, I.ORDER_COUNT " +
"FROM MEMBER M " +
"LEFT JOIN " +
" (SELECT IM.ID, COUNT(*) AS ORDER_COUNT " +
" FROM ORDERS O, MEMBER IM " +
" WHERE O.MEMBER_ID = IM.ID) I " +
"ON M.ID = I.ID";
Query nativeQuery = em.createNativeQuery(sql, "memberWithOrderCount");
List<Object[]> resultList = nativeQuery.getResultList();
for(Object[] row : resultList) {
Member member = (Member) row[0];
BigInteger orderCount = (BigInteger) row[1];
System.out.println("member = " + member);
System.out.println("orderCount = " + orderCount);
}
// 두번째 파라미터에 결과 매핑 정보의 이름이 사용되었다.
// 결과 매핑을 정의
@Entity
@SqlResultSetMapping(name = "memberWithOrderCount",
entities = {@EntityResult(entityClass = Member.class)},
column = {@ColumnResult(name = "ORDER_COUNT")})
public class Member {...}
// memberWithOrderCount의 결과 매핑을 잘 보면 회원 엔티티와 ORDER_COUNT 컬럼을 매핑했다.
// ID, AGE, NAME, TEAM_ID는 Member 엔티티와 매핑하고
// ORDER_COUNT는 단순히 값으로 매핑한다.
// 표준 명세 예제 - SQL
Query q = em.createNativeQuery(
"SELECT o.id AS order_id, " +
" o.quantity AS order_quantity, " +
" o.item AS order_item, " +
" i.name AS item_name, " +
"FROM Order o, Item i " +
"WHERE (order_quantity > 25) AND (order_item = i.id)", "OrderResults");
// 표준 명세 예제 - 매핑 정보
@SqlResultSetMapping(name = "OrderResults",
entities = {
@EntityResult(entityClass=com.acme.Order.class,
fields={
@FieldResult(name="id", column="order_id"),
@FieldResult(name="quantity", column="order_quantity", column="order_quantity"),
@FieldResult(name="item", column="order_item")
})},
columns={
@ColumnResult(name="item_name")}
)
// @FieldResult를 사용해서 컬럼명과 필드명을 직접 매핑한다.
// 이 설저은 엔티티의 필드에 정의한 @Column보다 앞선다.
// @FieldResult를 한번이라도 사용하면 전체 필드를 @FieldResult를 사용해야 한다.
// 두 엔티티를 조회하는데 컬럼명이 중복될 때도 @FieldResult를 사용해야 한다.
결과 매핑 어노테이션
@SqlResultSetMapping 속성
속성
기능
name
결과 매핑 이름
entities
@EntityResult를 사용해서 엔티티를 결과로 매핑한다.
columns
@ColumnResult를 사용해서 컬럼을 결과로 매핑한다.
@EntityResult 속성
속성
기능
entityClass
결과로 사용할 엔티티 클래스를 지정한다.
fields
`@FieldResult을 사용해서 결과 컬럼을 필드와 매핑한다.
discriminatorColumn
엔티티의 인스턴스 타입을 구분하는 필드(상속에서 사용됨)
@FieldResult 속성
속성
기능
name
결과를 받을 필드명
column
결과 컬럼명
@ColumnResult 속성
속성
기능
name
결과 컬럼명
2. Named 네이티브 SQL
// 엔티티 조회
@Entity
@NamedNativeQuery(
name = "Member.memberSQL",
query = "SELECT ID, AGE, NAME, TEAM_ID " +
"FROM MEMBER " +
"WHERE AGE > ?",
resultClass = Member.class)
public class Member {...}
// @NamedNativeQuery로 Named 네이티브 SQL을 등록했다.
// 사용예제
TypedQuery<Member> nativeQuery = em.createNamedQuery("Member.memberSQL", Member.class)
.setParameter(1, 20);
// JPQL Named 쿼리와 같은 createNamedQuery 메소드를 사용하는 것이다.
// TypedQuery를 사용할 수 있다.
// 결과 매핑 사용
@Entity
@SqlResultSetMapping( name = "memberWithOrderCount",
entities = {@EntityResult(entityClass = Member.class)},
column = {@ColumnResult(name = "ORDER_COUNT")}
)
@NamedNativeQuery(
name = "Member.memberWithOrderCount",
query = "SELECT M.ID, AGE, NAME, TEAM_ID, I.ORDER_COUNT " +
"FROM MEMBER M " +
"LEFT JOIN " +
" (SELECT IM.ID, COUNT(*) AS ORDER_COUNT " +
" FROM ORDERS O, MEMBER IM " +
" WHERE O.MEMBER_ID = IM.ID) I " +
"ON M.ID = I.ID",
resultSetMapping = "memberWithOrderCount"
)
public class Member {...}
// Named 네이티브 쿼리에서 resultSetMapping = "memberWithOrderCount"로 조회 결과를 매핑할 대상까지 지정
// Named 네이티브 쿼리를 사용하는 코드
List<Object[]> resultList = em.createNamedQuery("Member.memberWIthOrderCount")
.getResultList();
@NamedNativeQuery
속성
기능
name
네임드 쿼리 이름(필수)
query
SQL 쿼리(필수)
hints
벤더 종속적인 힌트
resultClass
결과 클래스
resultSetMapping
결과 매핑 사용
hints 속성은 SQL 힌트가 아니라 하이버네이트 같은 JPA 구현체에 제공하는 힌트다.
네이티브 SQL도 JPQL을 사용할 때와 마찬가지로 Query, TypeQuery(Named 네이티브 쿼리의 경우에만)를 반환한다.
따라서 JPQL API를 그대로 사용할 수 있다.
네이티브 SQL은 JPQL이 자동 생성하는 SQL을 수동으로 직접 정의하는 것이여서 JPA가 제공하는 기능을 대부분 그래로 사용할 수 있다.
네이티브 SQL은 관리하기 쉽지 않고 자주 사용하면 특정 데이터베이스에 종속적인 쿼리가 증가해 이식성이 떨어진다.
될 수 있으면 표준 JPQL을 사용하고 기능이 부족하면 차선책으로 하이버네이트 같은 JPA 구현체가 제공하는 기능을 사용하고 그래도 안되면 마지막 방법으로 네이티브 SQL을 사용하자.
6. 객체지향 쿼리 심화
1. 벌크 연산
// UPDATE 벌크 연산
String qlString = "update Product p " +
"set p.price = p.price * 1.1 " +
"where p.stockAmount < :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", 1000)
.executeUpdate();
// JPA 표준은 아니지만 하이버네이트 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)
.setParamter("price", 100)
.executeUpdate();
벌크 연산 주의점
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다.
// 벌크 연산 시 주의점 예쩨
// 상품A 조회(상품A의 가격은 1000원이다)
Product productA = em.createQuery("select p from Product p where p.name = :name", Product.class)
.setParameter("name", "productA")
.getSingleResult();
// 출력 결과: 1000
System.out.println("ProductA 수정 전 = " + productA.getPrice());
// 벌크 연산 수행으로 모든 상품 가격 10% 상승
em.createQuery("update Product p set p.price = p.price * 1.1")
.executeUpdate();
// 출력 결과: 1000
System.out.println("ProductA 수정 후 = " + productA.getPrice());
em.refresh() 사용
벌크 연산을 수행한 직후에 정확한 상품A 엔티티를 사용해야 한다면 em.refresh()를 사용해서 데이터베이스에서 상품A를 다시 조회하면 된다.
벌크 연산 먼저 실행
가장 실용적인 해결책은 벌크 연산을 가장 먼저 실행하는 것이다.
벌크 연산을 먼저 실행하고 나서 상품 A를 조회하면 벌크 연산으로 이미 변경된 상품A를 조회하게 된다.
이 방법은 JPA와 JDBC를 함께 사용할 떄도 유용하다.
벌크 연산 수행 후 영속성 컨텍스트 초기화
벌크 연산을 수행한 직후에 바로 영속성 컨텍스트를 초기화해서 영속성 컨텍스트에 남아 있는 엔티티를 제거하는 방법
엔티티를 조회할 때 영속성 컨텍스트에 남아 있는 엔티티를 조회할 수 있는데 이 엔티티에는 벌크 연산이 적용되어 있지 않다.
영속성 컨텍스트를 초기화하면 이후 엔티티를 조회할 때 벌크 연산이 적용된 데이터베이스에서 엔티티를 조회한다.
가능하면 벌크 연산을 가장 먼저 수행하는 것이 좋고 상황에 따라 영속성 컨텍스트를 초기화하는 것도 필요하다.
2. 영속성 컨텍스트와 JPQL
쿼리 후 영속 상태인 것과 아닌 것
JPQL로 엔티티를 조회하면 영속성 컨텍스트에서 관리되지만 엔티티가 아니면 영속성 컨텍스트에서 관리되지 않는다.
예를 들어 임베디드 타입은 조회해서 값을 변경해도 영속성 컨텍스트가 관리하지 않으므로 변경 감지에 의한 수정이 발생하지 않는다.
물론 엔티티를 조회하면 해당 엔티티가 가지고 있는 임베디드 타입은 함께 수정된다.
정리하면 조회한 엔티티만 영속성 컨텍스트가 관리한다.
JPQL로 조회한 엔티티와 영속성 컨텍스트
영속성 컨텍스트에 회원1이 있는데, JPQL로 회원1을 다시 조회한다면?
JPQL로 데이터베이스에서 조회한 엔티티가 영속성 컨텍스트에 이미 있으면 JPQL로 데이터베이스에서 조회한 결과를 버리고 대신 영속성 컨텍스트에 있던 엔티티를 반환한다.
이때 비교는 식별자 값을 사용해서 비교한다.
영속성 컨텍스트는 기본 키 값을 기준으로 엔티티를 관리해 기본 키 값이 같은 엔티티는 등록할 수 없다.
하지만 기존에 있던 수정 중인 데이터가 사라질 수 있으므로 위함하다. 따라서 영속성 컨텍스트는 기존 엔티티는 그대로 두고 새로 검색한 엔티티를 버린다.
find() vs JPQL
em.find() 메소드는 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾는다.
해당 엔티티가 영속성 컨텍스트에 있으면 메모리에서 바로 찾으므로 성능상 이점이 있다.(1차 캐시)
JPQL의 내부 동작방식은 조금 다른데 JPQL은 항상 데이터베이스에 SQL을 실행해서 결과를 조회한다.
해당 엔티티가 영속성 컨텍스트에 있으면 조회한 엔티티를 버리고 기존 엔티티를 반환한다.
3. JPQL과 플러시 모드
쿼리와 플러시 모드
JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 데이터베이스에서 데이터를 조회한다.
따라서 JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해야 한다. 그렇지 않으면 의도치 않은 결과가 발생할 수 있다.
// 쿼리와 플러시 모드 예제
product.setPrice(2000); // 가격을 1000 -> 2000원으로 변경
Product product2 = em.createQUery("select p from Product p where p.price = 2000", Product.class)
.getSingleResult();
// 플러시 모드 설정
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();
코드를 보면 첫 줄에서 플러시 모드를 COMMIT으로 설정했다. 따라서 쿼리를 실행할 때 플러시를 자동으로 호출하지 않는다.
만약 쿼리 실행 전에 플러시를 호출하고 싶으면 주석으로 처리해둔 em.flush()이 주석을 풀어 수동으로 플러쉬를 하거나
setFlushMode()로 해당 쿼리에서만 사용할 플러시 모드를 AUTO로 변경하면 된다.
플러시 모드의 기본값은 AUTO로 설정되어 있ㅋ으므로 일반적인 상황에서는 이런상황을 고민하지 않아도 된다.
그럼 왜 COMMIT 모드를 사용하는 것일까?
플러시 모드와 최적화
em.setFlushMode(FlushModeType.COMMIT)
FlushModeType.COMMIT 모드는 트랜잭션을 커밋할 때만 플러시하고 쿼리를 실행할 때는 플러시하지 않는다.
이런 상황은 자칫 데이터 무결성에 심각한 피해를 줄 수 있는데 그럼에도 플러시 모드가 너무 자주 일어나는 상황에서 이 모드를 사용하면 플러시 횟수를 줄여서 성능을 최적화할 수 있다.
JPA를 사용하지 않고 JDBC를 직접 사용해서 SQL을 실행할 때도 플러시 모드를 고민해야 한다.
JPA를 통하지 않고 JDBC로 쿼리를 직접 실행하면 JPA는 JDBC가 실행한 쿼리를 인식할 방법이 없다.
따라서 별도의 JDBC 호출은 플러시 모드를 AUTO로 설정해도 플러시가 일어나지 않는다.
이때는 JDBC로 쿼리를 실행하기 전에 em.flush()를 호출해서 영속성 컨텍스트의 내용을 데이터베이스에 동기화 하는 것이 안전하다.
10.4 QueryDSL
1. QueryDSL 설정
2. 시작
기본 Q 생성
3. 검색 조건 쿼리
4. 결과 조회
5. 페이징과 정렬
6. 그룹
7. 조인
8. 서브 쿼리
9. 프로젝션과 결과 반환
여러 컬럼 반환과 튜플
빈 생성
DISTINCT
10. 수정, 삭제 배치 쿼리
11. 동적 쿼리
12. 메소드 위임
5. 네이티브 SQL
JPQL은 대부분의 문법과 SQL 함수들을 지원하지만 특정 데이터베이스에 종속적인 기능은 지원하지 못함
특정 데이터베이스만 사용하는 함수
특정 데이터베이스만 지원하는 SQL 쿼리 힌트
인라인 뷰(From 절에서 사용하는 서브쿼리), UNION, INTERSECT
스토어 프로시저
특정 데이터베이스만 지원하는 문법
JPA가 지원하는 네이티브 SQL를 사용하는 이유는 엔티티를 조회할 수 있고 JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있기 때문이다.
1. 네이티브 SQL 사용
엔티티 조회
값 조회
결과 매핑 사용
결과 매핑 어노테이션
@SqlResultSetMapping
속성 속성@EntityResult
를 사용해서 엔티티를 결과로 매핑한다.@ColumnResult
를 사용해서 컬럼을 결과로 매핑한다.@EntityResult
속성 속성@FieldResult
속성 속성@ColumnResult
속성 속성2. Named 네이티브 SQL
@NamedNativeQuery
4. 네이티브 SQL 정리
6. 객체지향 쿼리 심화
1. 벌크 연산
벌크 연산 주의점
2. 영속성 컨텍스트와 JPQL
쿼리 후 영속 상태인 것과 아닌 것
JPQL로 조회한 엔티티와 영속성 컨텍스트
find() vs JPQL
3. JPQL과 플러시 모드
쿼리와 플러시 모드
플러시 모드와 최적화