본문 바로가기

IT공부/JPA

JPA 7강 - JPA 객체지향쿼리

JPA 7강 - JPA 객체지향쿼리

JPA는 다양한 쿼리 방법을 지원

  • JPQL
  • JPA Criteria
  • QueryDSL
  • 네이티브 SQL
  • JDBC API 직접 사용, MyBatis, StringJdbcTemplate 함께 사용

지금까지는 조회시 식별자를 이용해 하나만 조회했다. 예를 들어 15살 이상인 회원을 조회해야 한다면 식별자(ID)로 어떻게 하나하나 조회를 할 수 있을까? 결국 쿼리라는 개념이 필요하다. 그래서 JPA에서 쿼리를 지원하는게 JPQL이다. 

JPQL 소개

  • 가장 단순한 조회 방법
  • EntityManager.find()
  • 객체 그래프 탐색(a.getB().getC())
  • 나이가 18살 이상인 회원을 모두 검색하고 싶다면?

SQL과 똑같다. 단, 객체를 대상으로 조회한다는 차이점이 있다. 

JPQL

  • JPA를 사용하면 엔티티 객체를 중심으로 개발
  • 문제는 검색 쿼리
  • 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색

JPA는 DB를 모른다. 자바코드에서 DB를 모른다. member 객체가 있구나라고 머릿속으로 개발을 해야한다. MEMBER 테이블이 있다고 생각하고 개발하면 안된다. 그래서 모든 것을 객체대상으로 해야된다.)

 

  • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
  • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요
  • JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
  • SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
  • JPQL은 엔티티 객체를 대상으로 쿼리
  • SQL은 데이터베이스 테이블을 대상으로 쿼리 

//검색. 아래 쿼리는 member 엔티티를 select하는 쿼리이다.

String jpql = "select m From Member m where m.name like '%hello%'";

List<Member> result = em.createQuery(jpql, Member.class).getResultList();

  • 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존X
  • JPQL을 한마디로 정의하면 객체 지향 SQL

JPQL과 실행된 SQL

//검색

String jqpl = "select m from Member m where m.age > 18";

List<Member> result = em.createQuery(jpql, Member.class).getResultList();

 

실행된 SQL

select 

     m.id as id,

     m.age as age,

     m.USERNAME as USERNAME,

     m.TEAM_ID as TEAM_ID

from

     Member m

where

     m.age > 18

 

JPQL은 방언과 합쳐져서 현재 내 DB에 맞는 쿼리가 나가게 된다. 

JPQL 문법

  • select m from Member m where m.age > 18
  • 엔티티와 속성은 대소문자 구분(Member, username)
  • JPQL 키워드는 대소문자 구분 안함(SELECT, FROM, where)
  • 엔티티 이름을 사용, 테이블 이름이 아님(Member)
  • 별칭은 필수(m)

결과 조회

  • query.getResultList() : 결과가 하나 이상, 리스트 반환
  • query.getSingleResult() : 결과가 정확히 하나, 단일 객체 반환(정확히 하나가 아니면 예외발생)

파라미터 바인딩 - 이름기준, 위치기준

SELECT m FROM Member m where m.username=:username 

query.setParameter("username", usernameParam);

 

SELECT m FROM Member m where m.username=?1

query.setParameter(1, usernameParam);

 

웬만하면 이름으로 바인딩하자. 

프로젝션

  • SELECT m FROM Member m -> 엔티티 프로젝션
  • SELECT m.team FROM Member m -> 엔티티 프로젝션(team 객체 프로퍼티 조회)
  • SELECT username, age FROM Member m -> 단순 값 프로젝션(원래는 m.을 적어줘야 한다. 이거는 엔티티 조회가 아니다. )
  • new 명령어 : 단순 값을 DTO로 바로 조회(UserDTO로 바로 반환가능)

     SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m

 

  • DISTINCT는 중복 제거

페이징 API

  • JPA는 페이징을 다음 두 API로 추상화
  • setFirstResult(int startPosition) : 조회 시작 위치(0 부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수

페이징 API 예시

//페이징 쿼리

String jpql = "select m from Member m order by m.name desc";

List<Member>  resultList = em.createQuery(jpql, Member.class)

                        .setFirstResult(10)   

                        .setMaxResult(20)

                        .getResultList();

10번째부터 20개 가져오기(모든 DB 방언이 다 동작한다.)

페이징 API - MySQL 방언

SELECT 

    M.ID AS ID,

    M.AGE AS AGE,

    M.TEAM_ID AS TEAM_ID,

    M.NAME AS NAME

FROM

    MEMBER M

ORDER BY

    M.NAME DESC LIMIT ?, ?

페이징 API - Oracle 방언

SELECT * FROM

   ( SELECT ROW_.*, ROWNUM ROWNUM_

     FROM 

            ( SELECT 

                   M.ID AS ID,

                   M.AGE AS AGE,

                   M.TEAM_ID AS TEAM_ID,

                   M.NAME AS NAME

             FROM MEMBER M

             ORDER BY M.NAME

             ) ROW_

    WHERE ROWNUM <= ?

  )

WHERE ROWNUM_ >?

집합과 정렬

SELECT

   COUNT(m),     //회원수

   SUM(m.age),   //나이 합

   AVG(m.age),   //평균나이

   MAX(m.age),   //최대 나이

   MIN(m.age)    //최소 나이

from Member m

  • GROUP BY, HAVING
  • ORDER BY

조인

  • 내부조인 : SELECT m FROM Member m [INNER] JOIN m.team t
  • 외부조인 : SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
  • 세타조인 : select count(m) from Member m, Team t where m.username = t.name 전혀 연관관계가 없는 테이블끼리 조인하는 것을 말함.
  • 참고 : 하이버네이트 5.1부터 세타 조인도 외부 조인 가능

페치조인

  • 엔티티 객체 그래프를 한번에 조회하는 방법
  • 별칭을 사용할 수 없다.
  • JPQL : select m from Member m join fetch m.team -> member를 가져올 때 team까지 같이 가져옴
  • SQL : SELECT M.*, T.* FROM MEMBER T INNER JOIN TEAM T ON M.TEAM_ID = T.id

현업에서 페치조인을 많이 쓴다. LAZY로 설계하고 개발하다가 조인이 필요할 때 페치조인을 사용한다. 

페치조인 예시

String jpql = "select m from Member m join fetch m.team";

List<Member> members = em.createQuery(jpql, Member.class).getResultList();

for ( Member member : members ) {

     //페치조인으로 회원과 팀을 함께 조회해서 지연 로딩 발생 안함

     System.out.println("username = " + member.getUsername() + ", " + "teamname = " +                                         member.getTeam().name());

}

위와 같이 한방쿼리로 가져올 수 있다. member.getTeam().name()을 조회해도 레이지 로딩이 안걸린다. 예를 들어 리스트가 10개 있다고 하면 member.getTeam().name() 코드를 실행할 때마다 쿼리가 10번이 나간다. 성능이 안좋아진다. 이 문제를 바로 N + 1 문제라고 한다. N + 1이란 위 코드에서 쿼리에 fetch가 없고 Member만 레이지로 조회하는게 1이고, 추가로 루프 개수만큼이 N이 된다. 즉, 레이지로 인해 루프가 도는 횟수만큼 쿼리가 나가게 되므로 N이 되는 것이다. 이런 상황이 안생기도록 fetch join을 사용하자. 

JPQL 실습

Main.java

  1. try {
  2.        Team team = new Team();
  3.        team.setName("teamA");
  4.        em.persist(team);
  5.                      
  6.        Member member = new Member();
  7.        member.setName("hello");
  8.        member.setTeam(team);
  9.        em.persist(member);
  10.        team.getMembers().add(member);
  11.                      
  12.                      
  13.        String jpql = "select m From Member m where m.name like '%hello%'";
  14.        List<Member> result = em.createQuery(jpql, Member.class).getResultList();
  15.                      
  16.        tx.commit();
  17. } catch (Exception e) {

라인 13 ~ 14를 보면 JPQL 코드가 있다. 실행해보자.

위와 같이 SELECT 쿼리가 실행되는 것을 확인할 수 있다. 

이번에는 위 Main 클래스 코드에서 라인 13 쿼리를 "select m From Member m join fetch m.team where m.name like '%hello%'"로 변경하고 테스트 해보자. fetch join으로 Member와 team의 필드를 한 번에 조회하여 가져오는 것이다. 

위 실행결과와 같이 Member와 team 필드를 모두 가져왔다.

JPQL 기타

  • 서브 쿼리 지원
  • EXISTS, IN
  • BETWEEN, LIKE, IS NULL

JPQL 기본 함수

  • CONCAT
  • SUBSTRING
  • TRIM
  • LOWER, UPPER
  • LEGNTH
  • LOCATE
  • ABS, SQRT, MOD
  • SIZE, INDEX(JPA 용도)

CASE 식

기본 CASE 식

select 

   case when m.age <= 10 then '학생요금'

         when m.age >= 60 then '경로요금'

         else '일반요금'

   end

from Member m

 

단순 CASE 식

select

   case t.name

        when '팀A' then '인센티브110%'

        when '팀B' then '인센티브120%'

        else '인센티브105%'

from Team t

사용자 정의 함수 호출

  • 하이버네이트는 사용전 방언에 추가해야 한다.

select function('group_count', i.name) from Item i

Named 쿼리

  • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
  • 어노테이션, XML에 정의
  • 애플리케이션 로딩 시점에 초기화 후 재사용
  • 애플리케이션 로딩 시점에 쿼리를 검증

Named 쿼리 - 어노테이션

@Entity

@NamedQuery(

                 name = "Member.findByUsername",

                 query = "select m from Member m where m.username = :username")

public class Member {

     ...

}

 

List<Member> resultList = 

    em.createNamedQuery("Member.findByUsername", Member.class)

         . setParameter("username", "회원1")

         .getResultList();

쿼리에 대한 이름을 정해놓는 것이다. 그래서 쿼리 사용시 이름을 가지고 쿼리를 실행하는 것이다. Named 쿼리의 이점은 컴파일 시점에 쿼리에 대한 오류를 검증할 수 있다. 애플리케이션 로딩 시점에 쿼리를 파싱을 해서 실제 SQL로 바꾸기 때문에 문법 오류를 검증할 수 있는 것이다. 그래서 배포하기 전에 문제를 찾을 수 있다. 

페이징 실습

Main클래스 코드를 아래와 같이 수정하고 실행해보자.

  1. try {
  2.        Team team = new Team();
  3.        team.setName("teamA");
  4.        em.persist(team);
  5.                      
  6.        Member member = new Member();
  7.        member.setName("hello");
  8.        member.setTeam(team);
  9.        em.persist(member);
  10.        team.getMembers().add(member);
  11.                      
  12.                      
  13.        String jpql = "select m From Member m where m.name like '%hello%'";
  14.        List<Member> result = em.createQuery(jpql, Member.class)
  15.                                   .setFirstResult(10)
  16.                                   .setMaxResults(20)
  17.                                   .getResultList();
  18.                      
  19.        tx.commit();
  20. } catch (Exception e) {

라인 15 setFirstResult(10) 메서드는 10번째부터 가져온다는 의미이고 라인 16 setMaxResults(20)은 20개를 가져오겠다는 의미이다. 실행해보자.

실행결과를 보면 H2 방언에 맞게 쿼리를 실행하는 것을 확인할 수 있다. 

'IT공부 > JPA' 카테고리의 다른 글

JPA 8강 - Spring Data JPA와 QueryDSL 이해  (0) 2020.05.23
JPA 6강 - JPA 내부구조  (0) 2020.05.22
JPA 5강 - 양방향 매핑  (0) 2020.05.21
JPA 4강 - 연관관계 매핑  (0) 2020.05.21
JPA 3강 - 필드와 컬럼 매핑  (0) 2020.05.21