JPQL - Basic

JPQL 이란?

JPQL의 필요성

  • 일반적인 조회(em.find(), 객체그래프 탐색)가 아닌, 조건이 들어간 조회가 필요하다면?
  • 모든 데이터를 조회하고 앱단에서 걸러야 하나? → NO!
  • 필요한 데이터만 DB에서 불러 오는 것! (객체 중심으로 조회!) → 이 때 결국 검색 조건이 포함된 SQL이 필요JPQL 사용 필요!

JPQL

  • Java Persistence Query Language
  • SQL을 추상화한 객체 지향 쿼리 언어
  • SQL과 문법이 유사함 → SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN, …
  • 핵심은 Entity 객체를 대상으로 쿼리를 한다는 것 (SQL은 데이터베이스 테이블을 대상으로 쿼리) ⇒ JPQL을 SQL로 번역하여 실제 DB로 보냄
    • JPQL

        // String jpql = "select m from Member m where m.age > 18";
        List<Member> result = 
        			em.createQuery(
        					"select m from Member m where m.age > 18", 
        					Member.class).getResultList();
      
      • .cerateQuery() : JPQL을 통해 query 생성
      • jpql : Entity 객체를 중심으로 query를 생성할 수 있음
        • select m : 기존 SQL같은 경우 내가 조회할 항목(Col)들을 모두 작성해주어야 하지만 JPQL은 Entity 중심이기에 해당 Entity로 반환할 수 있음
      • Member.class : 반환할 Entity 지정
    • JPQL에 따라 실행된 SQL query

        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
      
  • 즉, 복잡한 쿼리를 사용할 때 발생할 수 있는 JPA와 DB의 패러다임 차이를 극복해주는 것
  • 실무에서 사용되는 복잡한 쿼리를 객체 지향적으로 짤 수 있도록 지원 → 즉, 조회 시 DB Table이 아닌 Entity 객체를 대상으로 검색
  • JPA가 지원하는 다양한 쿼리 방법 중 하나
    • JPA가 지원하는 다양한 쿼리
      • 자바 코드를 통해 JPQL을 동적으로 빌드해주는 Genterator (동적으로 query를 변경하게 할 수 있음)
        • JPA Criteria : JPA 표준 스펙. 사용이 복잡하며 실용성이 없음
        • QueryDSL : 오픈 소스. 실무에서 동적인 query를 짤 때 자주 사용하는 기술
      • Native SQL : JPQL을 사용해도 DB 종속적인 코드가 필요한데, 그 때 SQL 문법 그대로 사용할 수 있게끔 지원하는 것
  • JPQL은 JDBC API 직접 사용 or MyBatis, SpringJdbc와 함께 사용이 가능함
  • SQL을 추상화하기 때문에 특정 데이터베이스 SQL에 의존X

JPQL 기본 문법

기본 문법과 쿼리 API

  • JPQL 기본 query
    • SELECT 문

        select _
        from _
        [where _]
        [group by _]
        [having _]
        [order by _]
      
    • UPDATE 문

        update _
        [where _]
      
      • 벌크 연산과 연관 : JPA의 한건한건에 대한 변경감지에 의해 진행되는 update가 아닌, 여러건을 한방에 UPDATE하는 것
    • DELETE 문

        delete _
        [where _]
      
    • 기존 SQL과 굉장히 유사!
    • 하지만 JPQL은 Entity 객체를 대상으로 Query!!
  • JPQL 기본 문법
    • select m from Member as m where m.age > 18
    • SQL과 거의 동일하지만, Member가 Table을 대상으로 하는 것이 아니라 Entity 객체를 대상으로 한다는 것이 중요 → 그래서 m.age 처럼 속성값을 바로 사용할 수 있는 것
    • 또한 조회 자체를 Entity 객체로 반환할 수 있음
    • 엔티티와 속성은 대소문자 구분O (Member, age)
    • JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)
    • [중요] 별칭은 필수(m) (as는 생략가능)
  • 집합과 정렬

      select
      	 COUNT(m), -- 회원 수
      	 SUM(m.age), -- 나이 합
      	 AVG(m.age), -- 평균 나이
      	 MAX(m.age), -- 최대 나이
      	 MIN(m.age) -- 최소 나이
      from Member m
    
    • Group by, Having Order by 도 가능
  • TypedQuery vs Query [쿼리 반환]
    • TypedQuery : 반환 타입이 명확할 때 사용
      • TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
      • Select의 대상이 member entity 이므로 반환 타입이 명확함
    • Query : 반환 타입이 명확하지 않을 때 사용
      • Query query = em.createQuery("SELECT m.username, m.age from Member m");
      • m.username 은 String이고 m.age 는 int 로 두 개의 타입이 다르고, 이를 매핑할 타입이 존재하지 않음 → 반환 타입이 명확하지 않음
  • 결과 조회 API (Query API)
    • .getResultList() : 조회 결과가 하나 이상일 때 → 리스트로 반환
      • 결과 없을 시 빈 리스트 반환
    • .getSingleResult() : 결과가 정확히 하나일 때 → 단일 객체로 반환
      • 결과가 없을 시 오류 발생 → javax.persistence.NoResultException (Spring Data JPA(Spring과의 통합 JPA) 에서는 Null or Optional로 반환할 수 있도록 설정 됨)
      • 결과가 둘 이상일 시 오류 발생 → javax.persistence.NonUniqueResultException
  • 파라미터 바인딩

      em.createQuery(
      	"SELECT m FROM Member m where m.username=:username", Member.class)
      	.setParameter("username", usernameParam); // usernameParam 은 변수
    
    • 바인딩 시 “이름 기준”과 “위치 기준” 이 존재 하지만 “이름 기준”만 사용하는 것을 권장 (위의 예시는 “이름 기준” 바인딩)
    • :username : 파라미터 설정 → “:
    • .setParameter : 파라미터 바인딩

프로젝션 (SELECT)

  • SELECT 절에 조회할 대상을 지정하는 것
  • 대상
    • Entity 객체 프로젝션
      • em.createQuery("SELECT m FROM Member m", Member.class);
      • 조회되는 모든 Entity가 영속성 컨텍스트에 의해 관리됨
    • Entity 연관 객체 프로젝션
      • em.createQuery("SELECT m.team FROM Member m",Team.class);
      • SQL단에서 JOIN 절 자동 실행 (묵시적 JOIN) → DB입장에선 Member의 Team은 join해주어야 가져올 수 있음
      • 좋아 보이지만 예기치 못한 query(JOIN) 이기에 사용하지 않는 것이 좋음 → SQL과 비슷하게 join을 직접 사용해서 가져오는 것을 권장 (명시적 JOIN)
    • Embedded Type 객체 조회
      • em.createQuery("SELECT m.address FROM Member m",Address.class);
      • 값 타입이기에 묵시적 JOIN이 실행되지 않음 → DB입장에선 해당 Col이 그대로 있기 때문
      • 그저 반환이 Embedded Type으로 나오는 것
      • From Address a 와 같이 직접 조회는 불가능 → Entity가 아님! 소속되어 있는 객체임
    • Scala Type(int,Long,String, … 등 기본 데이터 타입) 조회
      • em.createQuery("SELECT m.username, m.age FROM Member m");
      • 해당 결과는 여러 Type(String, int)으로 조회됨 → 여러값 조회 방법 사용
  • Scala Type에서의 여러 값 조회
    1. Query 타입으로 조회
      • Query 타입(반환 타입이 명확하지 않은 상태)으로 받아온 후 Object로 빼오기
       Object singleResult = em.createQuery("select m.name, m.age from Member m").getSingleResult();
       Object[] resultOf = (Object[]) singleResult;
       for (Object o : resultOf) {
           System.out.println("result = " + o);
       }
      
    2. Object[] 타입으로 조회

       List<Object[]> resultList = em.createQuery("select m.name, m.age from Member m").getResultList();
       for (Object[] ol : resultList) {
           for (Object o : ol) {
       			System.out.println("result = ", o);
       		}
       }
      
    3. new 명령어로 조회 (DTO 사용) → BEST!!
      • DTO로 바로 조회
      • 패키지 명을 포함한 전체 클래스 명을 입력해줘야 함
      • 해당 조회되는 항목의 순서와 타입이 일치하는 생성자 필수
      • DTO 생성

          public class MemberDto {
              private String name;
              private int age;
              public MemberDto(String name, int age) {
                  this.name = name;
                  this.age = age;
              }
          		... // getter setter
          }
        
      • new 명령어 조회

          List<MemberDto> resultList = em.createQuery("select new hellojpql.dto.MemberDto(m.name, m.age) from Member m", MemberDto.class).getResultList();
                    
          for (MemberDto memberDto : resultList) {
              System.out.println("memberDto = " + memberDto.getName() + ", " + memberDto.getAge());
          }
        
  • DISTINCT로 중복제거 가능
    • SQL 단의 DISTINCT : 완전히 일치하는 row에 대한 중복 제거
    • JPA 단의 DISTINCT : 같은 식별자(Primary Key)를 가진 Entity에 대한 중복 제거
    • JPQL에서의 DISTINCT 는 두 기능을 모두 작동하여 중복 제거 실행

페이징 API

  • 페이징은 MySQL, ORACLE 등을 이용해서 직접 query를 짜려면 서로 다른 점도 있고 복잡함 (특히 ORACLE의 페이징은 굉장히 복잡함!) → But JPA는 SQL Dialect 존재
  • 하지만 JPA는 페이징을 두가지의 API로 추상화하여 쉽게 이용 가능
    • .setFirstResult() : 조회 시작 위치 (0부터 시작)
    • .setMaxResult() : 조회할 데이터 수
  • 예시

      em.createQuery("select m from Member m order by m.age desc", Member.class)
      	.setFirstResult(1) // offset 자동 추가 (offset = 1)
      	.setMaxResults(10) // limit 자동 추가 (limit = 10 -> 10개)
      	.getResultList(); // order by member.age desc limit ? offset ?
    
    • .setFirstResult(1)
      • 두번째 데이터부터 가져온다는 뜻 (인텍스 0부터 시작)
      • SQL로 보면 offsetoffset 1 을 자동 추가 해주는 것
    • setMaxResults(10)
      • 10개의 데이터를 가져온다는 뜻 → 2번째부터 10개의 데이터를 가져옴 (11번재 데이터까지 조회)
      • SQL로 보면 limit → limit 10 을 자동 추가 해주는 것
    • 결과 ⇒ select m from Member m order by member.age desc limit ? offset ?
    • 결과 SQL

        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 10 OFFSET 1
      

JOIN

  • 다양한 JOIN 지원
    • 내부 조인
      • SELECT m FROM Member m [INNER] JOIN m.team t
      • team이 없는 Member에 대해선 제외하고 조회됨
    • 외부 조인
      • SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
      • team이 없는 Member에 대해선 Null로 설정된 채로 조회됨
    • 세타 조인
      • select count(m) from Member m, Team t where m.username = t.name
      • 연관관계에 있지 않은 것들을 JOIN하는 것 (일명 막조인)
      • member가 3개고 team이 2개면 3x2의 row를 생성하는 join (일명 Cross JOIN)
  • ON 절을 활용하여 JOIN
    • JOIN 대상 필터링
      • on 을 통해 JOIN 시 조건을 걸어 원하는 대상만 JOIN할 수 있도록 필터링 하는 것
      • on t.name = ’A’ ⇒ ON m.TEAM_ID = t.id AND t.name = ‘A’
      • JPQL ⇒ em.createQuery(”SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'”)
      • SQL ⇒ SELECT m.* **t.*** FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'
    • 연관관계 없는 Entity 외부 조인
      • on 을 통해 서로 아무 연관관계가 없는 Entity라도 Cross Join이 가능
      • JPQL ⇒ em.createQuery(”SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name”)
      • SQL ⇒ SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name

서브 쿼리

  • 쿼리 속에 또 다른 쿼리를 만들어 적용하는 것
  • 예시
    • 나이가 평균 초과인 회원

        select m from Member m
        where m.age > (select avg(m2.age) from Member m2)
      
      • 서브 쿼리 : (select avg(m2.age) from Member m2)
      • 메인 쿼리와 서브 쿼리는 Entity를 공용으로 사용하면 안됨 → m2를 m으로 바꿔써 메인쿼리의 Entity를 공용으로 사용하면 X
    • 최소 한 건이라도 주문한 회원

        select m from Member m
        where (select count(o) from Order o where m = o.member) > 0
      
      • 서브 쿼리 : (select count(o) from Order o where m = o.member)
  • 지원 함수
    • **[NOT] EXISTS (subquery)** : 서브쿼리의 결과가 존재하면 참
      • ex) Team A 소속인 회원 → String query = "select m from Member m where exists (select t from m.team t where t.name = 'TeamA')";
    • **{ALL | ANY | SOME} (subquery)**
      • ALL : 모두 만족하면 참
        • ex) 전체 상품 각각의 재고보다 주문량이 많은 주문들 → select o from Order o where o.orderAmount > ALL (select p.stockAmount from Product p)
      • ANY, SOME : 조건을 하나라도 만족하면 참
        • ex) 어떤 팀이든 팀에 소속된 회원 → select m from Member m where m.team = ANY (select t from Team t)
    • **[NOT] IN (subquery)** : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
  • JPA 서브 쿼리의 한계
    • JPA 표준 에서는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능
    • 하이버네이트 구현체에서는 SELECT 절도 사용 가능! → select (select … )
    • FROM 절의 서브쿼리는 JPQL에선 불가능!
      • JOIN 으로 (풀 수 있으면) 풀어서 해결하는 것이 방법
      • 혹은 Native query로 직접 SQL문을 짜는 것도 방법

JPQL 타입 표현

  • 문자 : 작은따옴표 활용 → ‘Hello’ , ‘She’’s’
  • 숫자 : 자바 기본 숫자 표현 활용 → 10L(Long), 10D(Double), 10F(Float)
  • Boolean : TRUE, FALSE
  • ENUM : 패키지명 포함하여 활용jpabasic.StudyState.STUDYING
    • 파라미터를 활용하는 것도 가능 (자바 코드로 검증하는 것)

        String query = "select m from Member m where m.state = :studyState";
        em.createQuery(query, Member.class)
        				.setParameter("studyState", StudyState.STUDYING)
        				.getResultList();
      
  • Entity type : 타입 일치 여부 확인 (상속 관계에서 사용) → TYPE(m) = Member
    • 다형성을 자주 활용한다면 사용 가능
    • em.createQuery(”select i from Item i where type(i) = Book”, Item.class); → DTYPE으로 판단해 줌

조건식 (CASE 식)

  • 기본 CASE 식
    • 범위에 해당 하면 발동
      select
      	 case when m.age <= 10 then 'student'
      				when m.age >= 30 then 'adult'
      			  else 'normal'
      	 end
      from Member m
    
  • 단순 CASE 식
    • 정확히 맞아 떨어지면 발동
      select
      	 case t.name
      			 when 'TeamA' then '+110%'
      			 when 'TeamB' then '+120%'
      			 else '+105%'
      	 end
      from Team t
    
  • COALESCE
    • 하나씩 조회해서 null이면 정해진 값으로 반환
    • select coalesce(m.username,'No Name') from Member m
  • NULLIF
    • 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
    • select NULLIF(m.username, 'AMIN') from Member m

JPQL 기본 함수

  1. JPQL 표준 함수
    • CONCAT : 문자 더하기 → CONCAT('a', 'b') or 'a' || 'b'
    • SUBSTRING : 문자 자르기 → SUBSTRING(m.name, 2, 4)
    • TRIM : 공백 제거
    • LOWER, UPPER : 대소문자 로 변경
    • LENGTH : 길이
    • LOCATE : 위치 찾기 → LOCATE(’de’, ‘abcdefgh’) → 4
    • ABS, SQRT, MOD : 수학 함수
    • SIZE (JPA 용도) : 컬렉션의 크기 → SELECT SIZE(t.members) FROM Team t;
    • INDEX (JPA 용도) : @OrderColumn 사용 시의 INDEX 확인용
  2. 사용자 정의 함수
    • 사용 전 방언에 추가해주어야 함 (hibernate 구현체는)
    • 사용하는 DB Dialect를 상속받고, 사용자 정의 함수를 등록
    • 예시
      • H2 DB를 사용하면서 사용자 정의 함수를 등록하고자 할 때 (함수는 이미 만들었다고 가정)
      • 방언 생성 후 등록

          // 방언 생성
          public class MyH2Dialect extends H2Dialect {
          		public MyH2Dialect() {
          				regiterFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
          		}
          }
        
          <!-- 등록 -->
          <property name="hibernate.dialect" value="org.hibernate.dialect.MyH2Dialect"/>
        
      • 사용

          em.createQuery("select function('group_concat', i.name) from Item i")