JPQL

MINIMI·2023년 4월 10일

JPA

목록 보기
5/9
post-thumbnail

5-1. Simple

1) JPQL(Java Persistence Query Language)

  • 엔티티 객체를 중심으로 개발할 수 있는 객체 지향 쿼리
  • SQL보다 간겨랗며 DBMS에 상관 없이 개발 가능
    (방언을 통해 해결되며 해당 DBMS에 맞는 SQL 실행)
  • find() 메소드를 통한 조회와 다르게 항상 데이터베이스에 SQL을 실행해서 결과 조회
    (영속성 컨텍스트에 이미 존재하면 기존 엔티티를 반환하고 조회한 것은 버림)
  • JPQL은 엔티티 객체를 대상으로 쿼리를 질의하고 SQL은 데이터베이스의 테이블을 대상으로 질의 -> 결국 JPQL은 SQL로 변환
  • 기본 문법
    • SELECT
      • SELECT FROM WHERE GROUP BY HAVING ORDER BY
    • INSERT
      • ENTITYMANAGER가 제공하는 persist() 메소드 사용
    • UPDATE
      • update where
    • DELETE
      • DELETE WHERE
  • 특징
    • 엔티티와 속성은 대소문자 구분
    • SELECT, FROM 과 같은 기본 키워드는 대소문자 구분 안함
    • 별칭을 필수로 사용
    • 별칭 없이 작성 시 에러 발생
  • JPQL 사용 방법
    • 작성한 JPQL 문다열을 em.createQuery 메소드를 통해 쿼리 객체로 만듦
    • 쿼리 객체는 TypedQuery, Query 두 가지가 있음
    • TypedQuery
      • 반환할 타입을 명확하게 지정하는 방식
    • Query
      • 반환할 타입을 명확하게 지정할 수 없을 때 사용
    • 쿼리 객체에서 제공하는 메소드 getSingleResult() 또는 getResultList()를 호출해서 쿼리를 실행하고 데이터베이스 조회
    • getSingleResult()
      • 결과가 정확히 한 행일 경우
    • getResultList()
      • 결과가 2행 이상일 경우 사용하며 컬렉션 반환
@Test
	public void TypeQuery를_이용한_단일메뉴_조회_테스트() {
		
		//when
		String jpql = "SELECT m.menuName FROM section01_menu as m WHERE m.menuCode=7";
		TypedQuery<String> query = entityManager.createQuery(jpql, String.class);
		
		String resultMenuName = query.getSingleResult();
		
		//then
		assertEquals("민트미역국", resultMenuName);
	}
	
	@Test
	public void Query를_이용한_단일메뉴_조회_테스트() {
		
		//when
		String jpql = "SELECT m.menuName FROM section01_menu as m WHERE m.menuCode=7";
		Query query = entityManager.createQuery(jpql);		// 결과 값의 타입을 명시하지 않음
		
		Object resultMenuName = query.getSingleResult();	// 결과 값은 Object로 반환 된다
		
		//then
		assertTrue(resultMenuName instanceof String);
		assertEquals("민트미역국", resultMenuName);
	}
	
	@Test
	public void JPQL을_이용한_단일형_조회_테스트() {
		
		//when
		String jpql = "SELECT m FROM section01_menu as m WHERE m.menuCode=7";
		TypedQuery<Menu> query = entityManager.createQuery(jpql, Menu.class);		// 반환 타입을 row와 매핑할 엔티티 타입으로 설정
		
		Menu foundMenu = query.getSingleResult();
		
		//then
		assertEquals(7, foundMenu.getMenuCode());
		System.out.println(foundMenu);
	}
	
	@Test
	public void TypedQuery을_이용한_여러행_조회_테스트() {
		
		//when
		String jpql = "SELECT m FROM section01_menu as m";
		TypedQuery<Menu> query = entityManager.createQuery(jpql, Menu.class);		// 반환 타입을 row와 매핑할 엔티티 타입으로 설정
		
		List<Menu> foundMenuList = query.getResultList();
		
		//then
		assertNotNull(foundMenuList);
		foundMenuList.forEach(System.out::println);
	}
	
	@Test
	public void Query을_이용한_여러행_조회_테스트() {
		
		//when
		String jpql = "SELECT m FROM section01_menu as m ORDER BY m.menuCode";
		Query query = entityManager.createQuery(jpql);
		
		List<Menu> foundMenuList = query.getResultList();
		
		//then
		assertNotNull(foundMenuList);
		foundMenuList.forEach(System.out::println);
	}
	
	/* 연산자는 SQL과 다르지 않으므로 몇가지 종류의 연산자만 테스트 해본다. */
	
	@Test
	public void distinct를_활용한_중복제거_여러_행_조회_테스트() {
		
		//when
		String jpql = "SELECT DISTINCT m.categoryCode FROM section01_menu m ORDER BY m.categoryCode";
		TypedQuery<Integer> query = entityManager.createQuery(jpql, Integer.class);
		
		List<Integer> foundList = query.getResultList();
		
		//then
		assertNotNull(foundList);
		System.out.println(foundList);
	}
	
	@Test
	public void in_연산자를_활용한_조회_테스트() {
		
		/* categoryCode가 6, 10 인 메뉴 목록 조회 출력 */
		String jpql = "SELECT m FROM section01_menu m WHERE m.categoryCode IN (6,10)";
		Query query = entityManager.createQuery(jpql);
		
		List<Menu> foundList = query.getResultList();
		
		//then
		assertNotNull(foundList);
		foundList.forEach(System.out::println);
	}
	
	@Test
	public void like_연산자를_활용한_조회_테스트() {
		/* 마늘이 들어가는 메뉴명을 가진 메뉴 목록 조회 출력 */
		String jpql = "SELECT m FROM section01_menu m WHERE m.menuName like '%마늘%'";
		Query query = entityManager.createQuery(jpql);
		
		List<Menu> foundList = query.getResultList();
		
		//then
		assertNotNull(foundList);
		foundList.forEach(System.out::println);
	}

5-2. Parameter

  • 파라미터 바인딩 하는 법
    • 이름 기준 파라미터
      • : 다음에 이름 기준 파라미터 지정
    • 위치 기준 파라미터
      • ? 다음 값을 주고 위치 값은 1부터 시작
@Test
	public void 이름_기준_파라미터_바인딩_메뉴_목록_조회_테스트() {
		
		//given
		String menuNameParameter = "한우딸기국밥";
		
		//when
		String jpql = "SELECT m FROM section02_menu m WHERE m.menuName = :menuName";
		
		List<Menu> menuList = entityManager.createQuery(jpql, Menu.class)
										   .setParameter("menuName", menuNameParameter)
										   .getResultList();
		
		//then
		assertNotNull(menuList);
		menuList.forEach(System.out::println);
	}
	
	@Test
	public void 위치_기준_파라미터_바인딩_메뉴_목록_조회_테스트() {
		
		//given
		String menuNameParameter = "한우딸기국밥";
		
		//when
		String jpql = "SELECT m FROM section02_menu m WHERE m.menuName = ?1";
		List<Menu> menuList = entityManager.createQuery(jpql).setParameter(1, menuNameParameter).getResultList();
		
		//then
		assertNotNull(menuList);
		menuList.forEach(System.out::println);
	}

5-3. Projection

  • SELECT절에 조회할 대상을 지정하는 것
  • 엔티티 프로젝션
    • 원하는 객체 바로 조회 가능
    • 조회된 엔티티는 영속성 컨텍스트가 관리
  • 임베디드 타입 프로젝션
    • 엔티티와 거의 비슷하게 사용
    • 조회의 시작점이 될 수 없음(from절에 사용 불가)
    • 영속성 컨텍스트에서 관리 안됨
  • 스칼라 타입 프로젝션
    • 숫자, 문자, 날짜 같은 기본 데이터 타입
    • 영속성 컨텍스트에서 관리 안됨
  • new 명령어를 활용한 프로젝션
    • 다양한 종류의 값들을 단순 DTO로 바로 조회하는 방식
    • new 패키지명.DTO명을 쓰면 해당 DTO 바로 반환
    • new 명령어를 사용한 클래스의 객체는 엔티티가 아니므로 영속성 컨텍스트에서 관리 안됨
      참고 자료
      https://gitlab.com/java702/10.-jpa/chap05.jpql.git

5-4. Paging

public void 페이징_API를_이용한_조회_테스트() {
		
		//given
		int offset = 5;		//조회를 건너 뛸 행 수
		int limit = 5;		//조회할 행 수
		
		//when
		String jpql = "SELECT m FROM section04_menu m ORDER BY m.menuCode DESC";
		
		/* 쿼리 실행 결과를 보면 offset과 limit을 활용하는 문법으로 실행되어 있는데 이는 오라클 12버전 이후 추가된 문법이다.
		 * 오라클 11이하 버전에서는 rownum을 이용한 인라인뷰 방식으로 쿼리가 동작한다. */
		List<Menu> menuList = entityManager.createQuery(jpql, Menu.class).setFirstResult(offset)	//조회를 시작할 위치(0부터 시작)
																		 .setMaxResults(limit)		//조회할 데이터의 수
																		 .getResultList();
		
		//then
		assertNotNull(menuList);
		menuList.forEach(System.out::println);
	}

5-5. GoupFunction

  • COUNT, MAX, MIN, SUM, AVG로 SQL의 그룹함수와 별반 차이 없음
  • 주의사항
    • 그룹함수의 반환 타입은 결과 값이 정수이면 Long, 실수면 Double로 반환
    • 값이 없는 상태에서 count를 제외한 그룹 함수는 null이 되고 count만 0
      • 반환 값을 담기 위해 선언하는 변수 타입을 기본 자료형으로 하게 되면, 조회결과를 언박싱 할 때 NPE 발생
    • Having절에서 그룹 함수 결과값과 비교하기 위한 파라미터 타입은 Long or Double로 해야 함
@Test
	public void 특정_카테고리의_등록된_메뉴_수_조회() {
		
		//given
		int categoryCodeParameter = 3;
		
		//when
		String jpql = "SELECT COUNT(m.menuPrice) FROM section05_menu m WHERE m.categoryCode = :categoryCode";
		long countOfMenu = entityManager.createQuery(jpql, Long.class).setParameter("categoryCode", categoryCodeParameter).getSingleResult();
		
		//then
		assertTrue(countOfMenu >= 0);
		System.out.println("메뉴 갯수 = " + countOfMenu);
	}
	
	@Test
	public void count를_제외한_다른_그룹함수의_조회결과가_없는_경우_테스트() {
		
		//given
		int categoryCodeParameter = 2;
		
		//when
		String jpql = "SELECT SUM(m.menuPrice) FROM section05_menu m WHERE m.categoryCode = :categoryCode";
		
		
		//then
		assertThrows(NullPointerException.class, () -> {
			/* 반환 값을 담을 변수의 타입을 기본 자료형으로 하는 경우 Wrapper 타입을 언박싱 하는 과정에서 NPE 발생. */
			long sumOfPrice = entityManager.createQuery(jpql, Long.class).setParameter("categoryCode", categoryCodeParameter).getSingleResult();
		});
		
		assertDoesNotThrow(() -> {
			/* 반환 값을 담을 변수를 Wrapper 타입으로 선언해야 null 값이 반환 되어도 NPE가 발생하지 않음. */
			Long sumOfPrice = entityManager.createQuery(jpql, Long.class).setParameter("categoryCode", categoryCodeParameter).getSingleResult();
			
			System.out.println(sumOfPrice);
		});
	}
	
	@Test
	public void groupby절과_having절을_사용한_조회_테스트() {
		
		//given
		long minPrice = 50000L;	//그룹 함수의 반환 타입은 Long이므로 비교를 위한 파라미터도 Long 타입을 사용해야 한다.
		
		//when
		String jpql="SELECT m.categoryCode, SUM(m.menuPrice) FROM section05_menu m GROUP BY categoryCode HAVING SUM(m.menuPrice) >= :minPrice";
		List<Object[]> sumPriceOfCategoryList = entityManager.createQuery(jpql, Object[].class).setParameter("minPrice", minPrice).getResultList();
		
		//then
		assertNotNull(sumPriceOfCategoryList);
		sumPriceOfCategoryList.forEach(row -> {
			for(Object column : row) System.out.println(column + " ");
		});
	}

5-6. Join

  • 일반 조인
    • 일반적인 SQL조인 의미(내부조인, 외부조인, 컬렉션 조인, 세타조인)
  • 패치 조인
    • JPQL에서 성능 최적하를 위해 제공하는 기능으로 연관 된 엔티티나 컬렉션을 한 번에 조회 가능
    • 지연 로딩이 아닌 즉시 로딩을 수행
    • join fetch 명령어 사용
@Test
	public void 내부조인을_이용한_조회_테스트() {
		
		/* Menu 엔티티에 대한 조회만 일어나고 Category 엔티티에 대한 조회는 나중에 필요할때 일어난다. 
		 * select의 대상은 영속화하여 가져오지만 조인의 대상은 영속화하여 가져오지 않는다. */
		
		//when
		// 존재하는 카테고리 개수만큼 select / 지연로딩
		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);
	}
	
	@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";
		List<Object[]> menuList = entityManager.createQuery(jpql, Object[].class).getResultList();
		
		//then
		assertNotNull(menuList);
		menuList.forEach(row -> {
			Stream.of(row).forEach(col -> System.out.print(col + " "));
			System.out.println();
		});
	}
	
	@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();
		});
	}
	
	@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();
		});
	}
	
	@Test
	public void 패치조인을_이용한_조회_테스트() {
		
		/* 패치 조인하면 처음 SQL 실행 후 로딩할 때 조인 결과를 다 조회한 뒤에 사용하는 방식이기 때문에 쿼리 실행 횟수가 줄어들게 된다.
		 * 대부분의 경우 성능이 향상 된다. */
		
		//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);
	}

5-7. SubQuery

  • 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);
		
	}

5-8. NamedQuery

  • 동적 쿼리
    • 현재 우리가 사용하는 방식
    • EntityManager가 제공하는 메소드를 이용하여 JPQL 문자열로 런타임 시점에 동적으로 쿼리를 만드는 방식
    • 동적으로 만들어질 쿼리를 위한 조건식이나 반복문은 자바 코드 이용 가능
  • 정적 쿼리
    • 미리 쿼리를 정의하고 변경하지 않고 사용하는 쿼리
    • 미리 정의한 코드는 이름을 부여해서 사용
@Test
	public void 동적쿼리를_이용한_조회_테스트() {
		
		//given
		String searchName = "한우";
		int searchCategoryCode = 0;
		
		//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를 활용해도 좋다. */
	
	@Test
	public void 어노테이션_기반_네임드쿼리를_이용한_조회_테스트() {
		
		//when
		List<Menu> menuList = entityManager.createNamedQuery("section08_menu.selectMenuList", Menu.class).getResultList();
		
		assertNotNull(menuList);
		menuList.forEach(System.out::println);
	}
	
	/* 조금 더 복잡한 형태의 쿼리를 작성해야 하는 경우에는 xml 방식을 더 선호한다.(문자열로 쿼리를 작성하기 복잡하기 때문) */
	@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);
	}
  • namedQuery 설정
@Entity(name="section08_menu")
@Table(name="TBL_MENU")
@NamedQueries({
	@NamedQuery(name="section08_menu.selectMenuList", query="SELECT m FROM section08_menu m")
})
public class Menu {

    @Id
    @Column(name="MENU_CODE")
    private int menuCode;

    @Column(name="MENU_NAME")
    private String menuName;

    @Column(name="MENU_PRICE")
    private int menuPrice;

    @Column(name="CATEGORY_CODE")
    private int categoryCode;

    @Column(name="ORDERABLE_STATUS")
    private String orderableStatus;
profile
DREAM STARTER

0개의 댓글