오라클의 CONNECT BY처럼 특정 데이터베이스에 너무 종속적인 SQL 문법은 지원하지 않는다. 네이티브 SQL을 사용해야 한다.
JPA는 SQL을 직접 사용할 수 있는 기능을 제공하는데 이를 네이티브 SQL이라 한다.
네이티브 SQL을 사용하면 엔티티를 조회할 수 있고, 영속성 컨텍스트의 기능을 그대로 사용할 수 있다. JDBC API를 직접 사용하는 경우는 JPA의 기능을 사용할 수 없다.
10.5.1 네이티브 SQL 사용
엔티티 조회
em.createNativeQuery(SQL, 결과 클래스) 메서드를 사용하여 엔티티를 조회한다.
위치 기반 파라미터만 지원한다. 단, 하이버네이트에서는 이름 기반 파라미터를 지원한다.
@TestvoidprojectionEntity(){// 위치 기반 파라미터Stringsql1="select member_id, age, name, team_id from member where age > ?";List<Member>result1=em.createNativeQuery(sql1,Member.class).setParameter(1,20).getResultList();assertThat(result1.size()).isEqualTo(2);assertThat(result1).extracting("name").containsExactly("member3","member4");for(Membermember:result1){System.out.println("member = "+member);}// 이름 기반 파라미터Stringsql2="select member_id, age, name, team_id from member where age > :age";List<Member>result2=em.createNativeQuery(sql2,Member.class).setParameter("age",20).getResultList();assertThat(result2.size()).isEqualTo(2);assertThat(result2).extracting("name").containsExactly("member3","member4");for(Membermember:result2){System.out.println("member = "+member);}}
값 조회
em.createNativeQuery(SQL) 메서드의 두 번째 파라미터를 사용하지 않으면 값으로 조회한다.
값들은 Object[]로 반환한다.
영속성 컨텍스트가 관리하지 않는다.
@TestvoidprojectionValue(){Stringsql="select member_id, age, name, team_id from member where age > ?";List<Object[]>result=em.createNativeQuery(sql).setParameter(1,10).getResultList();assertThat(result).size().isEqualTo(3);for(Object[]row:result){System.out.println("member_id = "+row[0]);System.out.println("age = "+row[1]);System.out.println("name = "+row[2]);System.out.println("team_id = "+row[3]);}}
결과 매핑 사용
엔티티와 스칼라 값을 함께 조회하는 것처럼 매핑이 복잡해지면 @SqlResultSetMapping을 정의해서 결과 매핑을 사용한다.
@TestvoidresultMapping1(){Stringsql="select m.member_id, age, name, team_id, i.order_count "+"from member m "+"left join "+"(select im.member_id, count(*) as order_count "+"from orders o, member im "+"where o.member_id = im.member_id "+"group by o.member_id) i "+"on m.member_id = i.member_id";List<Object[]>result=em.createNativeQuery(sql,"memberWithOrderCount").getResultList();for(Object[]row:result){Membermember=(Member)row[0];BigIntegerorderCount=(BigInteger)row[1];System.out.println("member = "+member);System.out.println("orderCount = "+orderCount);}}
@TestvoidresultMapping2(){Stringsql="select o.order_id as order_id, "+"o.quantity as order_quantity, "+"o.item_id as order_item, "+"o.member_id as member_id, "+"i.name as item_name, "+"from orders o, item i "+"where (o.quantity > 25) and (o.item_id = i.item_id)";List<Object[]>result=em.createNativeQuery(sql,"OrderResults").getResultList();for(Object[]row:result){Orderorder=(Order)row[0];StringitemName=(String)row[1];System.out.println("order = "+order);System.out.println("itemName = "+itemName);}}
@Setter@Getter@NoArgsConstructor@ToString(exclude="team")@Entity@NamedNativeQueries({// 엔티티 조회@NamedNativeQuery(name="Member.memberSQL",query="select member_id, age, name, team_id from member where age > ?",resultClass=Member.class),// 결과 매핑 사용@NamedNativeQuery(name="Member.memberWithOrderCount",query="select m.member_id, age, name, team_id, i.order_count "+"from member m "+"left join "+"(select im.member_id, count(*) as order_count "+"from orders o, member im "+"where o.member_id = im.member_id "+"group by o.member_id) i "+"on m.member_id = i.member_id",resultSetMapping="memberWithOrderCount")})@SqlResultSetMapping(name="memberWithOrderCount",entities={@EntityResult(entityClass=Member.class)},columns={@ColumnResult(name="order_count")})publicclassMember{@Id@GeneratedValue@Column(name="member_id")privateLongid;privateStringname;privateintage;@ManyToOne(fetch=LAZY)@JoinColumn(name="team_id")privateTeamteam;publicMember(Stringname,intage,Teamteam){this.name=name;this.age=age;this.team=team;}}
@TestvoidnativeQueryPaging(){List<Member>result=em.createNativeQuery("select member_id, age, name, team_id from member",Member.class).setFirstResult(1).setMaxResults(2).getResultList();assertThat(result.size()).isEqualTo(2);assertThat(result).extracting("name").containsExactly("member2","member3");}
네이티브 SQL은 관리하기 쉽지 않고 특정 데이터베이스에 종속적인 쿼리가 증가한다.
될 수 있으면 표준 JPQL을 사용하고 기능이 부족하면 하이버네이트같은 JPA 구현체가 제공하는 기능을 사용하는게 좋다.
그래도 안되면 마지막 방법으로 네이티브 SQL을 사용하고 그래도 부족하다면 MyBatis나 JdbcTemplate같은 SQL 매퍼와 JPA를 함께 사용하는 것도 고려할만하다.
10.6 객체지향 쿼리 심화
10.6.1 벌크 연산
엔티티를 수정하려면 변경 감지 기능이나 병합을 사용한다.
엔티티를 삭제하려면 EntityManager.remove() 메서드를 사용한다.
여러개의 엔티티를 한 번에 수정하거나 삭제할 때는 벌크 연산을 사용한다.
벌크 연산은 executeUpdate() 메서드를 사용한다.
@Testvoidupdate(){intcount=em.createQuery("update Member m set m.age = 40 where m.age >= :age").setParameter("age",30).executeUpdate();assertThat(count).isEqualTo(2);}
@Testvoiddelete(){intcount=em.createQuery("delete from Order o").executeUpdate();assertThat(count).isEqualTo(5);}
벌크 연산의 주의점
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 SQL문을 전송한다.
따라서 영속성 컨텍스트에 있는 엔티티의 데이터와 데이터베이스의 데이터가 일치하지 않을 수 있다.
@Testvoidbulk(){MemberfoundMember=em.createQuery("select m from Member m where m.name = :name",Member.class).setParameter("name","member1").getSingleResult();assertThat(foundMember.getAge()).isEqualTo(10);intcount=em.createQuery("update Member m set m.age = :age").setParameter("age",40).executeUpdate();// 벌크 연산은 영속성 컨텍스트를 통하지 않고 데이터베이스에 직접 SQL문을 전송하므로,// 영속성 컨텍스트에 있는 회원과 데이터베이스에 있는 회원의 나이가 다를 수 있다.assertThat(foundMember.getAge()).isEqualTo(10);}
이러한 불일치를 해결하기 위해 아래와 같은 해결책을 사용할 수 있다.
em.refresh() 사용
벌크 연산을 수행한 직후에 엔티티를 사용해야 한다면 em.refresh() 메서드를 사용해서 데이터베이스에서 다시 조회한다.
벌크 연산 먼저 실행
벌크 연산 수행 후 영속성 컨텍스트 초기화
영속성 컨텍스트를 초기화하면 엔티티를 조회할 때 벌크 연산이 적용된 데이터베이스에서 엔티티를 다시 조회한다.
10.6.2 영속성 컨텍스트와 JPQL
쿼리 후 영속 상태인 것과 아닌 것
JPQL로 엔티티를 조회하면 영속성 컨텍스트에서 관리되지만 엔티티가 아니면 영속성 컨텍스트에서 관리되지 않는다.
Stringjpql1="select m from Member m"// 엔티티 조회, 영속성 컨텍스트에서 관리된다.Stringjpql2="select o.address from Order o"// 임베디드 타입 조회, 영속성 컨텍스트에서 관리되지 않는다.Stringjpql3="select m.username from Member m"// 단순 필드 조회, 영속성 컨텍스트에서 관리되지 않는다.
JPQL로 조회한 엔티티와 영속성 컨텍스트
JPQL로 데이터베이스에서 조회한 엔티티가 영속성 컨텍스트에 이미 있으면 JPQL로 데이터베이스에서 조회한 결과를 버리고, 영속성 컨텍스트에 있던 엔티티를 반환한다.
find() vs JPQL
find() 메소드는 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾는다.
JPQL은 항상 데이터베이스에 SQL을 실행해서 결과를 조회한다.
10.6.3 JPQL과 플러시 모드
플러시는 영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화 한다. 플러시가 일어날 때 영속성 컨텍스트에 등록, 수정, 삭제한 엔티티를 찾아서 INSERT, UPDATE, DELETE SQL을 만들어 데이터베이스에 반영한다.
플러시는 플러시 모드에 따라 커밋하기 직전 또는 쿼리 실행 직전에 자동으로 호출된다.
플러시 모드는 FlushModeType.AUTO가 기본 값이다. (커밋 직전 또는 쿼리 실행 직전에 자동으로 호출한다.)
FlushModeType.COMMIT 옵션은 커밋시에만 플러시를 호출한다. 쿼리시에는 호출하지 않는다.
쿼리와 플러시 모드
JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 데이터베이스에서 데이터를 조회하므로, JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해야 한다.
플러시 모드가 AUTO이면 JPQL 쿼리 실행 직전에 플러시되어 영속성 컨텍스트의 내용이 데이터베이스에 동기화된다.
플러시 모드가 COMMIT이면 JPQL 쿼리 실행시 영속성 컨텍스트에 반영된 데이터를 조회할 수 없다.
COMMIT 모드에서 쿼리 실행 전에 플러시를 호출하고 싶으면 em.flush() 메서드를 호출하거나, setFlushMode() 메서드로 해당 쿼리에서만 사용 할 플러시 모드를 AUTO로 변경한다.
플러시 모드와 최적화
FlushModeType.COMMIT 모드는 트랜잭션을 커밋할 때만 플러시하고 쿼리를 실행할 때는 플러시하지 않는다.
따라서 해당 모드에서 JPA 쿼리를 사용할 때 영속성 컨텍스트에 있지만 데이터베이스에 반영하지 않은 데이터는 조회할 수 없다.
하지만 상황에 따라 플러시가 너무 자주 일어나는 상황에 이 모드를 사용하면 플러시 횟수를 줄여서 최적화 할 수 있다.
JDBC를 직접 사용하여 SQL을 실행할 때 플러시 모드를 AUTO로 설정해도 플러시가 일어나지 않는다. 따라서 JDBC로 쿼리를 실행하기 직전에 em.flush()를 호출해서 영속성 컨텍스트의 내용을 데이터베이스에 동기화 하는 것이 안전하다.
Leave a comment