💁♀️ 네이티브 쿼리란,
SQL을 개발자가 직접 정의해서 사용할 수 있도록 해주는 수동모드
JPQL은 데이터베이스들이 따로 지원하는 것들에 있어 모든 것을 SQL로 자동으로 바꿔주지 않음 (인라인 뷰, UNION, INTERSECT 등등)
쉽게 말해, 어떠한 다양한 이유로 JPQL을 사용할 수 없는 경우나 SQL쿼리를 최적화 해서 데이터 베이스의 성능을 향상시킬 때 JPA는 Native SQL을 통해 SQL을 직접 사용할 수 있는 기능을 제공
🙋 JDBC API와의 차이점
직접 SQL을 작성하는 JDBC API와는 달리 네이티브 SQL은 JPA의 영속성 컨텍스트의 기능을 그대로 사용 가능
🙋 네이티브 쿼리 API의 사용
- 결과 타입 정의
public Query createNativeQuery(String sqlString, Class resultClass);
- 결과 타입을 정의할 수 없을 때
public Query createNativeQuery(String sqlString);
- 결과 매핑 사용
public Query createNativeQuery(String sqlString, String resultSetMapping);
@Test
public void 결과_타입을_정의한_네이티브_쿼리_사용_테스트() {
// given
int menuCodeParameter = 144;
// when
/* 우리가 사용하는 DBMS의 고유한 SQL 문법을 작성. 위치 기반 파라미터로만 사용이 가능!! */
String query = "SELECT MENU_CODE, MENU_NAME, MENU_PRICE, CATEGORY_CODE, ORDERABLE_STATUS "
+ "FROM TBL_MENU WHERE MENU_CODE = ?";
/* 일부 컬럼만 조회하는 것은 불가능 */
// String query = "SELECT MENU_CODE, MENU_NAME, MENU_PRICE, CATEGORY_CODE, "
// + "FROM TBL_MENU WHERE MENU_CODE = ?";
/* 주의 !!!
* 모든 컬럼값을 매핑하는 경우에만 타입을 특정(ex: Manu.class)할 수 있음. 일부 컬럼만 조회하고 싶은 경우는 Object[] 또는 스칼라 값을 별도로 담을 클래스를 정의해서 사용해야함 */
Query nativeQuery = entityManager.createNativeQuery(query, Menu.class)
.setParameter(1, menuCodeParameter); /* 위치 기반 */
Menu foundMenu = (Menu) nativeQuery.getSingleResult(); /* 다운캐스팅 */
// then
assertNotNull(foundMenu);
assertTrue(entityManager.contains(foundMenu)); /* 영속성 컨텍스트에서 관리하는 객체인지 확인 (엔티티로 처리했기 때문에 True) */
System.out.println(foundMenu);
}
@Test
public void 결과_타입을_정의할_수_없는_경우_조회_테스트() {
// when
String query = "SELECT MENU_NAME, MENU_PRICE FROM TBL_MENU";
List<Object[]> menuList = entityManager.createNativeQuery(query) /* 엔티티 타입이 아닌 경우, 타입 작성 XXX (작성 시 오류) */
.getResultList();
/* 결과 타입을 특정하는 것 자체가 불가능. 엔티티로 매핑 시킬 경우에만 클래스 타입을 입력 */
// List<Object[]> menuList = entityManager.createNativeQuery(query, Object[].class).getResultList();
//then
assertNotNull(menuList);
menuList.forEach(row -> {
Stream.of(row).forEach(col -> System.out.print(col + " "));
System.out.println();
});
}
@SqlResultSetMappings
@SqlResultSetMapping
@EntityResult
@ColumnResult
@FieldResult
@Entity(name="section01_category")
@Table(name="TBL_CATEGORY")
/* MENU_COUNT는 가공된 컬럼이므로 따로 매핑 처리를 해줘야함 */
@SqlResultSetMappings(
value= {
/* 1. @Column으로 매핑 설정이 되어있는 경우 - 자동 엔티티 매핑 */
@SqlResultSetMapping(
name = "categoryCountAutoMapping", // 결과 매핑 이름
entities = {@EntityResult(entityClass = Category.class)}, // @EntityResult를 사용해서 엔티티를 결과로 매핑
columns = {@ColumnResult(name = "MENU_COUNT")} // @ColumnResult를 사용해서 컬럼을 결과로 매핑
),
/* 2. 매핑 설정을 수동으로 해주는 경우 - 필드 위의 @Column은 생략 가능 */
@SqlResultSetMapping(
name = "categoryCountManualMapping",
entities = {
@EntityResult(entityClass = Category.class, fields = {
@FieldResult(name = "categoryCode", column = "CATEGORY_CODE"),
@FieldResult(name = "categoryName", column = "CATEGORY_NAME"),
@FieldResult(name = "refCategoryCode", column = "REF_CATEGORY_CODE")
})
},
columns = {@ColumnResult(name = "MENU_COUNT")}
)
}
)
public class Category {
@Id
@Column(name="CATEGORY_CODE")
private int categoryCode;
@Column(name="CATEGORY_NAME")
private String categoryName;
@Column(name="REF_CATEGORY_CODE")
private Integer refCategoryCode;
/* 기본 셋팅 */
}
@Test
public void 자동_결과_매핑을_사용한_조회_테스트() {
//when
String query = "SELECT"
+ " A.CATEGORY_CODE, A.CATEGORY_NAME, A.REF_CATEGORY_CODE, NVL(V.MENU_COUNT, 0) MENU_COUNT"
+ " FROM TBL_CATEGORY A"
+ " LEFT JOIN (SELECT COUNT(*) AS MENU_COUNT, B.CATEGORY_CODE "
+ " FROM TBL_MENU B"
+ " GROUP BY B.CATEGORY_CODE) V ON (A.CATEGORY_CODE = V.CATEGORY_CODE)"
+ " ORDER BY 1";
Query nativeQuery = entityManager.createNativeQuery(query, "categoryCountAutoMapping"); /* Category 엔티티의 ResultSet안의 name 속성 */
List<Object[]> categoryList = nativeQuery.getResultList();
//then
assertTrue(entityManager.contains(categoryList.get(0)[0])); /* categoryList 안의 첫 번째 객체의 0번 인덱스가 영속성 컨텍스트에서 관리가 되는지 확인 */
assertNotNull(categoryList);
categoryList.forEach(row -> {
Stream.of(row).forEach(col -> System.out.print(col + " "));
System.out.println();
});
}
@Test
public void 수동_결과_매핑을_사용한_조회_테스트() {
//when
String query = "SELECT"
+ " A.CATEGORY_CODE, A.CATEGORY_NAME, A.REF_CATEGORY_CODE, NVL(V.MENU_COUNT, 0) MENU_COUNT"
+ " FROM TBL_CATEGORY A"
+ " LEFT JOIN (SELECT COUNT(*) AS MENU_COUNT, B.CATEGORY_CODE "
+ " FROM TBL_MENU B"
+ " GROUP BY B.CATEGORY_CODE) V ON (A.CATEGORY_CODE = V.CATEGORY_CODE)"
+ " ORDER BY 1";
Query nativeQuery = entityManager.createNativeQuery(query, "categoryCountManualMapping"); /* Category 엔티티의 ResultSet안의 name 속성 */
List<Object[]> categoryList = nativeQuery.getResultList();
//then
assertTrue(entityManager.contains(categoryList.get(0)[0])); /* categoryList 안의 첫 번째 객체의 0번 인덱스가 영속성 컨텍스트에서 관리가 되는지 확인 */
assertNotNull(categoryList);
categoryList.forEach(row -> {
Stream.of(row).forEach(col -> System.out.print(col + " "));
System.out.println();
});
}
🙋 네이티브 SQL도 JPQL처럼
@NamedNativeQuery
를 사용해 정적 SQL을 만들어두고 사용 가능
@NamedNativeQueries
@NamedNativeQuery
/* Category 엔티티 */
@Entity(name="section02_category")
@Table(name="TBL_CATEGORY")
@SqlResultSetMapping(name = "categoryCountAutoMapping2", // section1의 매핑 이름과 다른 이름을 주어야 함
entities = {@EntityResult(entityClass = Category.class)},
columns = {@ColumnResult(name = "MENU_COUNT")}
)
@NamedNativeQueries(
value = {
@NamedNativeQuery(
name = "Category.menuCountOfCategory",
query = "SELECT"
+ " A.CATEGORY_CODE, A.CATEGORY_NAME, A.REF_CATEGORY_CODE, NVL(V.MENU_COUNT, 0) MENU_COUNT"
+ " FROM TBL_CATEGORY A"
+ " LEFT JOIN (SELECT COUNT(*) AS MENU_COUNT, B.CATEGORY_CODE"
+ " FROM TBL_MENU B\n"
+ " GROUP BY B.CATEGORY_CODE) V ON (A.CATEGORY_CODE = V.CATEGORY_CODE)"
+ " ORDER BY 1",
resultSetMapping = "categoryCountAutoMapping2" // 위의 @SqlResultSetMapping의 name 속성과 동일하게 작성
)
}
)
public class Category {
}
@Test
public void NamedNativeQuery를_이용한_조회_테스트() {
// when
Query nativeQuery = entityManager.createNamedQuery("Category.menuCountOfCategory"); /* Category 엔티티의 @NamedNativeQuery의 name 속성 */
List<Object[]> categoryList = nativeQuery.getResultList();
//then
assertTrue(entityManager.contains(categoryList.get(0)[0]));
assertNotNull(categoryList);
categoryList.forEach(row -> {
Stream.of(row).forEach(col -> System.out.print(col + " "));
System.out.println();
});
}
💻 Mini Console
Category{categoryCode=4, categoryName='한식', refCategoryCode=1} 6
Category{categoryCode=5, categoryName='중식', refCategoryCode=1} 3
Category{categoryCode=6, categoryName='일식', refCategoryCode=1} 4
Category{categoryCode=7, categoryName='퓨전', refCategoryCode=1} 5
...
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
<named-native-query name="Category.menuCountOfCategoryXML" result-set-mapping="categoryCountXmlMapping">
<query>
SELECT
A.CATEGORY_CODE
, A.CATEGORY_NAME
, A.REF_CATEGORY_CODE
, NVL(V.MENU_COUNT, 0) MENU_COUNT
FROM TBL_CATEGORY A
LEFT JOIN (SELECT COUNT(*) AS MENU_COUNT
, B.CATEGORY_CODE
FROM TBL_MENU B
GROUP BY B.CATEGORY_CODE
) V ON (A.CATEGORY_CODE = V.CATEGORY_CODE)
ORDER BY 1
</query>
</named-native-query>
<sql-result-set-mapping name="categoryCountXmlMapping">
<entity-result entity-class="com.greedy.section02.named.Category"/>
<column-result name="MENU_COUNT"/>
</sql-result-set-mapping>
</entity-mappings>
@Test
public void xml_NamedNativeQuery를_이용한_조회_테스트() {
// when
Query nativeQuery = entityManager.createNamedQuery("Category.menuCountOfCategoryXML"); /* xml파일의 named-native-query의 name 속성 */
List<Object[]> categoryList = nativeQuery.getResultList();
//then
assertTrue(entityManager.contains(categoryList.get(0)[0]));
assertNotNull(categoryList);
categoryList.forEach(row -> {
Stream.of(row).forEach(col -> System.out.print("[" + col + "]"));
System.out.println();
});
}
💻 Mini Console
[Category{categoryCode=4, categoryName='한식', refCategoryCode=1}][6]
[Category{categoryCode=5, categoryName='중식', refCategoryCode=1}][3]
[Category{categoryCode=6, categoryName='일식', refCategoryCode=1}][4]
[Category{categoryCode=7, categoryName='퓨전', refCategoryCode=1}][5]
...