BOOK 3 - 자바 ORM 표준 JPA 프로그래밍(10)-3
객체 지향 쿼리 언어 - 네이티브 SQL & 객체지향 쿼리 심화
네이티브 SQL
- JPQL은 표준 SQL이 지원하는 대부분의 문법과 SQL 함수들을 지원하지만 특정 데이터베이스에 종속적인 기능은 지원하지 않는다.
- JPA 구현체들은 특정 데이터베이스에 종속적인 기능을 다음과 같이 지원한다.
- 특정 데이터베이스만 지원하는 함수
- JPQL에서 네이티브 SQL 함수를 호출할 수 있다.
- 데이터베이스 Dialect에 각 데이터베이스에 종속적인 함수들을 정의해두었다.
- 직접 호출할 함수를 정의할 수도 있다.
- 특정 데이터베이스만 지원하는 SQL 쿼리 힌트
- 하이버네이트를 포함한 몇몇 JPA 구현체들이 지원한다.
- 인라인 뷰(FROM 절에서 사용하는 서브 쿼리), UNION, INTERSECT
- 하이버네이트는 지원하지 않는다.
- 스토어드 프로시저
- JPQL에서 스토어드 프로시저를 호출할 수 있다.
- 특정 데이터베이스만 지원하는 문법
- 네이티브 SQL을 사용한다.
- 특정 데이터베이스만 지원하는 함수
- JPQL을 사용할 수 없을 때 JPA는 SQL을 직접 사용할 수 있는 기능을 제공하는데, 이것을 네이티브 SQL이라 한다.
네이티브 SQL 사용
엔티티 조회
- 네이티브 SQL은
em.createNativeQuery(SQL, 결과 클래스)를 사용한다.- 쿼리는 위치 기반 파라미터만 지원한다.
- 하이버네이트는 네이티브 SQL에 이름 기반 파라미터를 사용할 수 있다.
- 쿼리는 위치 기반 파라미터만 지원한다.
String sql = "SELECT ID, AGE, NAME, TEAM_ID FROM MEMBER WHERE AGE > ?";
Query nativeQuery = em.createNativeQuery(sql, Member.class).setParameter(1, 20);
List<Member> resultList = nativeQuery.getResultList();
- 네이티브 SQL로 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
값 조회
String sql = "SELECT ID, AGE, NAME, TEAM_ID FROM MEMBER WHERE AGE > ?";
Query nativeQuery = em.createNativeQuery(sql).setParameter(1, 20);
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()메소드에 두번째 파라미터를 넘겨주지 않으면 된다. - JPA는 조회한 값들을
Object[]에 담아서 반환한다.- 스칼라 값들을 조회했을 뿐이므로 조회한 결과를 영속성 컨텍스트가 관리하지 않는다.
결과 매핑 사용
- 엔티티와 스칼라 값을 함께 조회하는 것 처럼 결과값 매핑이 복잡해지면
@SqlResultSetMapping을 정의해서 사용한다.
@Entity
@SqlResultSetMapping(name = "memberWithOrderCount",
entities = {@EntityResult(entityClass = Member.class)},
columns = {@ColumnResult(name="ORDER_COUNT")})
public class Member { ... }
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];
}
- 쿼리 결과에서
ID,AGE,NAME,TEAM_ID는Member엔티티와 매핑하고,ORDER_COUNT는 단순히 값으로 매핑한다. - 컬럼명과 필드명을 직접 매핑하려면
@FieldResult를 사용한다.name: 결과를 받을 필드명column: 결과 컬럼명
@SqlResultSetMapping(name = "OrderResults",
entities = {
@EntityResult(entityClass=Order.class, fields = {
@FieldResult(name="id", column="order_id"),
@FieldResult(name="quantity", column="order_quantity"),
@FieldResult(name="item", colum="order_item")})},
columns = {@ColumnResult(name="item_name")})
public class Order { ... }
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"
);
Named 네이티브 SQL
- 네이티브 SQL도 Named 네이티브 SQL을 사용해서 정적 SQL을 작성할 수 있다.
@NamedNativeQuery로 Named 네이티브 SQL을 등록한다.
@Entity
@NamedNativeQuery(name="Member.memberSQL",
query="SELECT ID, AGE, NAME, TEAM_ID FROM MEMBER WHERE AGE > ?",
resultClass=Member.class)
public class Member { ... }
-
name: Named 쿼리 이름 -
query: SQL 쿼리 -
resultClass: 결과 클래스 -
resultSetMapping: 결과 매핑 사용 -
등록한 Named 네이티브 SQL은 다음과 같이 사용한다.
TypedQuery<Member> nativeQuery = em.createNamedQuery("Member.memberSQL", Member.class) .setParameter(1, 20);- JQPL Named 쿼리와 같은
createNamedQuery()메서드를 사용한다.
- JQPL Named 쿼리와 같은
-
Named 네이티브 SQL에서 결과 매핑을 사용할 수도 있다.
@Entity
@SqlResultSetMapping(name="memberWithOrderCount",
entities={@EntityResult(entityClass=Member.class)},
columns={@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 { ... }
네이티브 SQL 정리
-
네이티브 SQL도 JPQL과 마찬가지로
Query,TypedQuery를 반환한다.-
따라서 JPQL API를 그대로 사용할 수 있다.
-
JPQL처럼 페이징 처리 API를 적용할 수 있다.
String sql = "SELECT ID, AGE, NAME, TEAM_ID FROM MEMBER"; Query nativeQuery = em.createNativeQuery(sql, Member.class) .setFirstResult(10) .setMaxResults(20);
-
-
네이티브 SQL은 JPQL이 자동 생성하는 SQL을 수동으로 직접 정의하는 것이다.
- 따라서 JPA가 제공하는 기능 대부분을 그대로 사용할 수 있다.
-
네이티브 SQL은 관리하기 쉽지 않고 이식성이 떨어지기 때문에 될 수 있으면 표준 JPQL을 사용하고 마지막 방법으로 네이티브 SQL을 사용하는 것이 좋다.
객체지향 쿼리 심화
벌크 연산
- 수백개 이상의 엔티티를 하나씩 처리하기에는 시간이 오래걸리므로 여러 건을 한번에 수정/삭제하는 벌크 연산을 사용한다.
String sql = "update Product p set p.price = p.price * 1.1 " +
"where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(sql)
.setParameter("stockAmount", 10)
.executeUpdate();
-
executeUpdate(): 벌크 연산을 수행하고 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다.- 삽입 , 삭제 시에도 같은 메소드를 사용한다.
String sql = "delete from Product p where p.price < :price"; int resultCount = em.createQuery(sql) .setParameter("price", 1000) .executeUpdate();
벌크 연산의 주의점
- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리하므로 주의해야 한다.
- 예를 들어, 데이터베이스에 가격이 1000원인
productA가 있다고 하자.
Product productA = em.createQuery("select p from Product p where p.name = 'productA'", Product.class)
.getSingleResult();
System.out.println("productA price : " + productA.getPrice());
// productA price : 1000
em.createQuery("update Product p set p.price = p.price * 1.1")
.executeUpdate();
System.out.println("productA price : " + productA.getPrice());
// productA price : 1000
- 벌크 연산으로 데이터베이스의
productA를 포함한 모든 상품의 가격을 10% 인상했지만, 업데이트 이후에도productA의 가격이 1000원으로 출력된다. - 현재
productA엔티티 인스턴스는 영속성 컨텍스트에서 관리되고, 벌크 연산은 영속성 컨텍스트를 통하지 않고 데이터베이스에 바로 쿼리한다.- 따라서 영속성 컨텍스트에 있는
productA와 데이터베이스에 있는productA의 가격이 다를 수 있다.
- 따라서 영속성 컨텍스트에 있는
- 이런 문제를 다양한 방법으로 해결할 수 있다.
em.refresh() 사용
- 벌크 연산을 수행한 직후에 정확한
productA엔티티를 사용해야 한다면em.refresh()를 사용해서 데이터베이스에서productA를 다시 조회한다.em.refresh(productA)
벌크 연산 먼저 실행
- 벌크 연산을 먼저 실행하고 나서
productA를 조회하면 벌크 연산으로 이미 변경된 가격의 상품이 조회된다.
벌크 연산 수행 후 영속성 컨텍스트 초기화
- 벌크 연산을 수행한 직후에 바로 영속성 컨텍스트를 초기화해서 영속성 컨텍스트에 남아있는 엔티티를 제거한다.
- 영속성 컨텍스트를 초기화하면 이후 엔티티를 조회할 때 벌크 연산이 적용된 데이터베이스에서 엔티티를 조회한다.
영속성 컨텍스트와 SQL
쿼리 후 영속 상태인 것과 아닌 것
-
JPQL로 엔티티를 조회하면 영속성 컨텍스트에서 관리되지만, 엔티티가 아니면 영속성 컨텍스트에서 관리되지 않는다.
- 예를 들어 임베디드 타입은 조회해서 값을 변경해도 영속성 컨텍스트가 관리하지 않으므로 dirty checking에 의한 수정이 발생하지 않는다.
select m from Member m // 엔티티 조회 (관리 o) select o.address from Order o // 임베디드 타입 조회 (관리 x) select m.id, m.username from Member m // 단순 필드 조회 (관리 x)
JPQL로 조회한 엔티티와 영속성 컨텍스트
member1이 이미 영속성 컨텍스트에 존재하는 상태에서 JPQL로 다시 조회하는 경우를 보자.
em.find(Member.class, "member1");
List<Member> resultList = em.createQuery("select m from Member m", Member.class)
.getResultList();
- JPQL로 데이터베이스에서 조회한 엔티티가 영속성 컨텍스트에 이미 있으면 조회한 결과를 버리고 대신에 영속성 컨텍스트에 있던 엔티티를 반환한다.
- JPQL을 사용해서 조회를 요청한다.
- JPQL은 SQL로 변환되어 데이터베이스를 조회한다.
- 조회한 결과와 영속성 컨텍스트를 비교한다.
- 식별자 값을 기준으로
member1은 이미 영속성 컨텍스트에 있으므로 버리고 기존에 있던member1이 반환 대상이 된다. - 식별자 값을 기준으로
member2는 영속성 컨텍스트에 없으므로 영속성 컨텍스트에 추가한다. - 쿼리 결과인
member1,member2를 반환한다.
데이터베이스에서 새로 조회한 결과로 영속성 컨텍스트의 엔티티를 대체하지 않는 이유는 영속성 컨텍스트에서 수정 중인 데이터가 사라질 수 있으므로 위험하기 때문이다.
find() vs JPQL
em.find()는 엔티티를 영속성 컨텍스트에서 먼저 찾고, 없으면 데이터베이스에서 찾는다.- 따라서 해당 엔티티가 영속성 컨텍스트에 있으면 메모리에서 바로 찾으므로 성능상 이점이 있다.
- JPQL은 항상 데이터베이스에 SQL을 실행해서 결과를 조회한다.
JPQL과 플러시 모드
-
플러시는 영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화하는 것이다.
-
플러시 모드에 따라 커밋하기 직전이나 쿼리 실행 직전에 자동으로 플러시가 호출된다.
em.setFlushMode(FlushModeType.AUTO); // 커밋 또는 쿼리 실행시 플러시(기본값) em.setFlushMode(FlushModeType.COMMIT); // 커밋시에만 플러시
쿼리와 플러시 모드
- JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 데이터베이스에서 데이터를 조회한다.
- 따라서 JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해야 한다.
- 플러시 모드를 따로 설정하지 않으면 플러시모드가
AUTO이므로 쿼리 실행 직전에 영속성 컨텍스트가 플러시 된다.