조인 JOIN
조인이 종류
1. 일반 조인 : 일반적인 SQL 조인을 의미 (내부 조인, 외부 조인, 컬렉션 조인, 세타 조인 a.k.a. 크로스 조인)
2. 페치 조인 : JPQL에서 성능 최적화를 위해 제공하는 기능으로, 연관된 엔티티나 컬렉션을 한 번에 조회할 수 있다.
지연 로딩이 아닌 즉시 로딩을 수행하며 join fetch 명령어를 사용한다.
내부 조인
: Menu 엔티티에 대한 조회만 일어나고 Category 엔티티에 대한 조회는 나중에 필요할 때 일어난다. (지연 로딩)
select의 대상은 영속화하여 가져오지만 조인의 대상은 영속화하여 가져오지 않는다.
@Test
public void 내부조인을_이용한_조회_테스트 () {
//when
String jpql = "SELECT m FROM section06_menu m JOIN m.category c"; //항상 필드를 기준으로~
List<Menu> menuList = entityManager.createQuery(jpql, Menu.class).getResultList();
//then
assertNotNull(menuList);
menuList.forEach(System.out::println); // 조회는 메뉴만, 카테고리는 지연 로딩, 카테고리 개수만큼 조회
}
select category0_.CATEGORY_NAME as col_0_0_,
menulist1_.MENU_NAME as col_1_0_
from TBL_CATEGORY category0_ l
eft outer join TBL_MENU menulist1_
on category0_.CATEGORY_CODE=menulist1_.CATEGORY_CODE
외부 조인
@Test
public void 외부조인을_이용한_조회_테스트() {
//when
String jpql = "SELECT m.menuName, c.categoryName FROM section06_menu m RIGHT JOIN m.category c "
+ "ORDER BY m.category.categoryCode"; //category 필드의 categoryCode
List<Object[]> menuList = entityManager.createQuery(jpql, Object[].class).getResultList();
//then
assertNotNull(menuList);
menuList.forEach(row -> {
Stream.of(row).forEach(col -> System.out.print(col + " "));
//배열, 컬렉션 Stream으로 변환가능, 값이 없을 경우 null반환
System.out.println(); //행 간
});
}
Hibernate:
select menu0_.MENU_NAME as col_0_0_,
category1_.CATEGORY_NAME as col_1_0_
from TBL_MENU menu0_
right outer join TBL_CATEGORY category1_
on menu0_.CATEGORY_CODE=category1_.CATEGORY_CODE order by menu0_.CATEGORY_CODE
컬렉션 조인
: 컬렉션 조인은 의미상 분류된 것으로 컬렉션을 지니고 있는 엔티티를 기준으로 조인하는 것을 말한다.
@Test
public void 컬렉션조인을_이용한_조회_테스트() {
//when
String jpql = "SELECT c.categoryName, m.menuName FROM section06_category c LEFT JOIN c.menuList m";
List<Object[]> categoryList = entityManager.createQuery(jpql, Object[].class).getResultList();
//then
assertNotNull(categoryList);
categoryList.forEach(row -> {
Stream.of(row).forEach(col -> System.out.print(col + " "));
System.out.println();
});
}
Hibernate:
select category0_.CATEGORY_NAME as col_0_0_,
menulist1_.MENU_NAME as col_1_0_
from TBL_CATEGORY category0_
left outer join TBL_MENU menulist1_
on category0_.CATEGORY_CODE=menulist1_.CATEGORY_CODE
세타 조인
: 조인되는 모든 경우의 수를 다 반환하는 크로스 조인과 같다.
@Test
public void 세타조인을_이용한_조회_테스트() {
//when
String jpql = "SELECT c.categoryName, m.menuName FROM section06_category c, section06_menu m";
List<Object[]> categoryList = entityManager.createQuery(jpql, Object[].class).getResultList();
//then
assertNotNull(categoryList);
categoryList.forEach(row -> {
Stream.of(row).forEach(col -> System.out.print(col + " "));
System.out.println();
});
}
Hibernate:
select category0_.CATEGORY_NAME as col_0_0_,
menu1_.MENU_NAME as col_1_0_
from TBL_CATEGORY category0_ cross
join TBL_MENU menu1_
페치 조인
: 페치 조인을 하면 처음 SQL 실행 후 로딩할 때 조인 결과를 다 조회한 뒤에 사용하는 방식이기 때문에 쿼리 실행 횟수가 줄어들게 된다. 대부분의 경우 성능이 향상 된다.
@Test
public void 페치조인을_이용한_조회_테스트 () {
//when
String jpql = "SELECT m FROM section06_menu m JOIN FETCH m.category c";
List<Menu> menuList = entityManager.createQuery(jpql, Menu.class).getResultList();
//then
assertNotNull(menuList);
menuList.forEach(System.out::println);
}
Hibernate:
select menu0_.MENU_CODE as menu_code1_1_0_,
category1_.CATEGORY_CODE as category_code1_0_1_,
menu0_.CATEGORY_CODE as category_code2_1_0_,
menu0_.MENU_NAME as menu_name3_1_0_,
menu0_.MENU_PRICE as menu_price4_1_0_,
menu0_.ORDERABLE_STATUS as orderable_status5_1_0_,
category1_.CATEGORY_NAME as category_name2_0_1_,
category1_.REF_CATEGORY_CODE as ref_category_code3_0_1_
from TBL_MENU menu0_
inner join TBL_CATEGORY category1_ on menu0_.CATEGORY_CODE=category1_.CATEGORY_CODE
서브쿼리 Subquery
: JPQL도 SQL 처럼 서브 쿼리를 지원한다.
하지만 select, from절에서는 사용할 수 없고 where, having절에서만 사용이 가능하다.
@Test
public void 서브쿼리를_이용한_메뉴_조회_테스트() {
//given
String categoryNameParameter = "한식";
//when
String jpql = "SELECT m FROM section07_menu m WHERE m.categoryCode "
+ "= (SELECT c.categoryCode FROM section07_category c WHERE c.categoryName = :categoryName)";
List<Menu> menuList = entityManager.createQuery(jpql, Menu.class)
.setParameter("categoryName", categoryNameParameter)
.getResultList();
//then
assertNotNull(menuList);
menuList.forEach(System.out::println);
}
네임드쿼리
- 동적쿼리 : 현재 우리가 사용하는 방식처럼 EntityManager가 제공하는 메소드를 이용하여 JPQL을 문자열로 런타임 시점에 동적으로 쿼리를 만드는 방식
(동적으로 만들어질 쿼리를 위한 조건식이나 반복문은 자바 코드를 이용할 수 있다.)
- 정적쿼리 : 미리 쿼리를 정의하고 변경하지 않고 사용하는 쿼리를 말하며, 미리 정의한 코드는 이름을 부여해서 사용하게 된다.
-> 이를 NamedQuery라고 한다. 어노테이션 방식과 xml방식 두 가지가 있는데 쿼리가 복잡할수록 xml 방식 선호
1. 동적쿼리
(동적쿼리는 주로 검색 필터링에 사용한다.)
@Test
public void 동적쿼리를_이용한_조회_테스트() {
//given
String searchName = "한우";
int searchCategoryCode = 4;
//when
StringBuilder jpql = new StringBuilder("SELECT m FROM section08_menu m ");
if(searchName != null && !searchName.isEmpty() && searchCategoryCode > 0) {
jpql.append("WHERE ");
jpql.append("m.menuName LIKE '%' || :menuName || '%' ");
jpql.append("AND ");
jpql.append("m.categoryCode = :categoryCode ");
} else {
if(searchName != null && !searchName.isEmpty()) {
jpql.append("WHERE ");
jpql.append("m.menuName LIKE '%' || :menuName || '%' ");
} else if(searchCategoryCode > 0) {
jpql.append("WHERE ");
jpql.append("m.categoryCode = :categoryCode ");
}
}
TypedQuery<Menu> query = entityManager.createQuery(jpql.toString(), Menu.class);
if(searchName != null && !searchName.isEmpty() && searchCategoryCode > 0) {
query.setParameter("menuName", searchName);
query.setParameter("categoryCode", searchCategoryCode);
} else {
if(searchName != null && !searchName.isEmpty()) {
query.setParameter("menuName", searchName);
} else if(searchCategoryCode > 0) {
query.setParameter("categoryCode", searchCategoryCode);
}
}
List<Menu> menuList = query.getResultList();
//then
assertNotNull(menuList);
menuList.forEach(System.out::println);
}
동적 SQL을 작성하기에 JPQL은 많이 어렵다. 컴파일 에러가 발생하는 것이 아니기 때문에 쿼리를 매번 실행해서 확인해야 하는 불편함이 있다.
Criteria나 queryDSL을 활용하면 보다 편리하게 작성할 수 있으며, 쿼리 작성 시 컴파일 에러로 잘못된 부분을 확인할 수 있어 작성하기도 편하다.
마이바티스를 혼용하거나 마이바티스의 구문 빌더 API를 활용해도 좋다.
2. 정적쿼리 - 어노테이션 방식
(문자열로 쿼리를 작성하기 복잡)
@Entity(name="section08_menu")
@Table(name="TBL_MENU")
@NamedQueries({
@NamedQuery(name="section08_menu.selectMenuList", query="SELECT m FROM section08_menu m")
})
public class Menu {
생략
▼ createNamedQuery 사용
@Test
public void 어노테이션_기반_네임드쿼리를_이용한_조회_테스트() {
//when
List<Menu> menuList = entityManager.createNamedQuery("section08_menu.selectMenuList", Menu.class).getResultList();
//then
assertNotNull(menuList);
menuList.forEach(System.out::println);
}
3. 정적쿼리 - xml 방식
조금 더 복잡한 형태의 쿼리를 작성해야 하는 경우에는 xml방식을 더 선호한다.
1. META-INF 폴더 하위에 menu-query.xml

2.persistence.xml 파일에 <mapping-file> 추가

3. activation, jaxb-impl 라이브러리 추가
@Test
public void xml기반_네임드쿼리를_이용한_조회_테스트() {
//given
int menuCodeParameter = 21;
//when
Menu foundMenu = entityManager.createNamedQuery("section08_menu.selectMenuNameByCode", Menu.class)
.setParameter("menuCode", menuCodeParameter)
.getSingleResult();
//then
assertNotNull(foundMenu);
System.out.println("foundMenu = " + foundMenu);
}
'LECTURE > JPA' 카테고리의 다른 글
Spring data JPA (0) | 2023.04.14 |
---|---|
페이징 Paging, 그룹함수 (0) | 2023.04.11 |
JPA association mapping (0) | 2023.04.10 |
JPA 개요 및 영속성 컨텍스트 (0) | 2023.04.06 |