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_IDMember 엔티티와 매핑하고, 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() 메서드를 사용한다.
  • 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로 데이터베이스에서 조회한 엔티티가 영속성 컨텍스트에 이미 있으면 조회한 결과를 버리고 대신에 영속성 컨텍스트에 있던 엔티티를 반환한다.
    1. JPQL을 사용해서 조회를 요청한다.
    2. JPQL은 SQL로 변환되어 데이터베이스를 조회한다.
    3. 조회한 결과와 영속성 컨텍스트를 비교한다.
    4. 식별자 값을 기준으로 member1 은 이미 영속성 컨텍스트에 있으므로 버리고 기존에 있던 member1 이 반환 대상이 된다.
    5. 식별자 값을 기준으로 member2 는 영속성 컨텍스트에 없으므로 영속성 컨텍스트에 추가한다.
    6. 쿼리 결과인 member1 , member2 를 반환한다.

데이터베이스에서 새로 조회한 결과로 영속성 컨텍스트의 엔티티를 대체하지 않는 이유는 영속성 컨텍스트에서 수정 중인 데이터가 사라질 수 있으므로 위험하기 때문이다.

find() vs JPQL

  • em.find() 는 엔티티를 영속성 컨텍스트에서 먼저 찾고, 없으면 데이터베이스에서 찾는다.
    • 따라서 해당 엔티티가 영속성 컨텍스트에 있으면 메모리에서 바로 찾으므로 성능상 이점이 있다.
  • JPQL은 항상 데이터베이스에 SQL을 실행해서 결과를 조회한다.

JPQL과 플러시 모드

  • 플러시는 영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화하는 것이다.

  • 플러시 모드에 따라 커밋하기 직전이나 쿼리 실행 직전에 자동으로 플러시가 호출된다.

    em.setFlushMode(FlushModeType.AUTO);		// 커밋 또는 쿼리 실행시 플러시(기본값)
    em.setFlushMode(FlushModeType.COMMIT);	// 커밋시에만 플러시	
    

쿼리와 플러시 모드

  • JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 데이터베이스에서 데이터를 조회한다.
    • 따라서 JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해야 한다.
  • 플러시 모드를 따로 설정하지 않으면 플러시모드가 AUTO 이므로 쿼리 실행 직전에 영속성 컨텍스트가 플러시 된다.