조인 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

+ Recent posts