본문 바로가기
Backend/JPA

[JPQL] 객체 지향 쿼리 기본 문법과 기능

by 2245 2023. 12. 24.

출처

https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 강의 - 인프런

현업에서 실제로 JPA로 개발을 하고 있습니다. 그런 입장에서보면 지금 작성하고 있는 코드들이 어떻게 작동하는지 이해하는데 큰 도움을 주는 강의입니다. 다음은 제가 느낀 이 강의의 장점들

www.inflearn.com

 

목차

     

    JPQL 소개

    • JPQL은 객체 지향 쿼리 언어입니다. 즉, 테이블 대상이 아닌 엔티티를 대상으로 쿼리를 날립니다.
    • JPQL은 결국 SQL로 변환됩니다. 
    • JPQL은 SQL을 추상화했기 때문에 특정 데이터베이스에 의존하지 않습니다. 

    객체 모델과 DB 모델의 차이

     

    JPQL 기본 

    select_문 :: =
     select_절
     from_절
     [where_절]
     [groupby_절]
     [having_절]
     [orderby_절]
    
    update_문 :: = update_절 [where_절]
    delete_문 :: = delete_절 [where_절]

    [예시]

    select m from Member as m where m.age > 18
    • SQL과 유사합니다. 
    • 엔티티와 속성은 대소문자를 구분합니다. (ex) Member, int age)
    • JPQL 키워드는 대소문자를 구분하지 않습니다. (ex) Select, FROM, where)
    • 테이블이 아닌 엔티티 이름을 사용합니다. (ex) Member)
    • 별칭은 필수입니다. (ex) fMember as m)
      • as 키워드는 생략이 가능합니다. (ex) Member m)

     

    JPQL와 SQL

    • SQL과 문법이 거의 같습니다. 아래의 문법을 그대로 사용할 수 있습니다. 
    • EXISTS, IN
    • AND, OR, NOT
    • =, >, >=, <, <=, <>
    • BETWEEN, LIKE, IS NULL

     

    집계 함수, 집합, 정렬

    • 집계 함수
    select
       COUNT(m), //회원수
       SUM(m.age), //나이 합
       AVG(m.age), //평균 나이
       MAX(m.age), //최대 나이
       MIN(m.age) //최소 나이
    from Member m
    • 집합 - GROUP BY, HAVING
    • 정렬 - ORDER BY

     

     

    TypeQuery, Query

    • TypeQuery: 반환 타입이 명확할 때 사용합니다. 
    • Query: 반환 타입이 명확하지 않을 때 사용합니다.
    TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
    Query query = em.createQuery("SELECT m.username, m.age from Member m"); 
    //username은 string, age는 int 이기 떄문에 이 둘을 묶어서 명확한 타입을 지정할 수 없다.

     

    결과 조회 API

    • query.getResultList(): 결과가 하나 이상일 때 리스트를 반환합니다.
      • 결과가 없다면 빈 리스트를 반환합니다. 
    TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
    List<Member> resultList = query.getResultList();
    • query.getSingleResult(): 결과가 정확히 하나 즉, 단일 객체 반환일 때 사용합니다.
      • 결과가 없는 경우: javax.persistence.NoResultException 예외 발생
      • 둘 이상일 경우: javax.persistence.NonUniqueResultException 예외 발생
    TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m where m.id=10", Member.class);
    Member result = 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);

     

     

    엔티티 직접 사용

    기본 키

    • JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용합니다. 
    //JPQL
    select count(m.id) from Member m /*엔티티의 아이디를 사용*/
    select count(m) from Member m   /*엔티티를 직접 사용*/
    
    //SQL - 위의 JPQL 둘 다 같은 SQL 실행
    select count(m.id) from Member m

    [파라미터로 엔티티 전달 vs 식별자 직접 전달]

    //파라미터로 엔티티 전달
    String jpql = “select m from Member m where m = :member”;
    Member findMember = em.createQuery(jpql)
                         .setParameter("member", member)
                         .getSingleResult();
                         
    //파라미터로 식별자 직접 전달
    String jpql = “select m from Member m where m.id = :memberId”;
    Member findMember = em.createQuery(jpql)
                         .setParameter("memberId", memberId)
                         .getSingleResult();

    [실행된 SQL]

    select m.* from Member m where m.id=?

     

    외래 키

    [파라미터로 엔티티 전달 vs 식별자 직접 전달]

    Team team = em.find(Team.class, 1L);
    
    //파라미터로 엔티티 전달
    String qlString = “select m from Member m where m.team = :team”;
    List resultList = em.createQuery(qlString)
                         .setParameter("team", team)
                         .getResultList();
                         
     //파라미터로 식별자 직접 전달
     String qlString = “select m from Member m where m.team.id = :teamId”;
    List resultList = em.createQuery(qlString)
                         .setParameter("teamId", teamId)
                         .getResultList();

    [실행된 SQL]

    select m.* from Member m where m.team_id=?

     

     

    페이징 API

    JPA는 페이징을 다음 두 API로 추상화하여 제공합니다. 

    • setFirstResult(int startPosition) : 조회 시작 위치 지정 (0부터 시작)
    • setMaxResults(int maxResult) : 조회할 데이터 수 지정

    [예시]

    //페이징 쿼리
     String jpql = "select m from Member m order by m.name desc";
     List<Member> resultList = em.createQuery(jpql, Member.class)
                 .setFirstResult(10)
                 .setMaxResults(20)
                 .getResultList();

     

    페이징 API는 방언에 따라 다른 SQL을 생성합니다. 

    [MySQL]

    Hibernate: 
      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 ?, ?

    [Oracle]

    Hibernate: 
      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 desc
           ) ROW_
          WHERE ROWNUM <= ?
        )
      WHERE ROWNUM_ > ?

     

     

    JPQL 타입 표현

    • 문자: ‘HELLO’, ‘She’’s’
      • 문자는 작은 따옴표(') 안에 작성되어야 합니다.
      • 작은 따옴표(')의 이스케이프 문자는 작은 따옴표(') 입니다.
    • 숫자: 10L(Long), 10D(Double), 10F(Float)
    • Boolean: TRUE, FALSE
    String query = "select m.username, 'HELLO', TRUE From Member m";
    List<Object[]> result = em.createQuery(query)
            .getResultList();
    
    for (Object[] objects : result) {
        System.out.println("objects = " + objects[0]);  //teamA
        System.out.println("objects = " + objects[1]);  //HELLO
        System.out.println("objects = " + objects[2]);  //true
    }
    • enum: jpabook.MemberType.Admin
      • enum을 사용할 땐 패키지명을 포함해야 합니다. 
    package hellojpa.jpql;
    
    public enum MemberType {
        ADMIN, USER
    }
    "select m from Member m where m.memberType=hellojpa.jpql.MemberType.ADMIN"
    String query = "select m from Member m where m.memberType=:userType";
    List<Object[]> result = em.createQuery(query)
                                .setParameter("usreType", MemberType.ADMIN)
                                .getResultList();
    • TYPE(i) = Book
      • 엔티티 타입에 해당하는 타입을 조회할 수 있습니다.
      • 상속 관계에서 사용합니다. 
    em.createQuery("select i from Item i where type(i) = Book", Item.class);
    select *
    from
      Item item
    where
      item.DTYPE='BOOK'

     

     

    다형성 쿼리

    TYPE

    • 조회 대상을 특정 자식으로 한정합니다. 
    • 예) Item 중에 Book, Movie를 조회해라
    //JPQL
    select i from Item i
    where type(i) IN (Book, Movie)
    
    //SQL
    select i from i
    where i.DTYPE in (‘B’, ‘M’)

     

    TREAT(JPA 2.1)

    • 자바의 타입 캐스팅과 유사합니다. 
    • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용합니다. 
    • FROM, WHERE, SELECT(하이버네이트 지원) 사용
    • 예) 부모인 Item과 자식 Book이 있다.
    //JPQL
    select i from Item i
    where treat(i as Book).author = ‘kim’  //다운 캐스팅 (author 속성은 Book에만 존재)
    
    //SQL
    select i.* from Item i
    where i.DTYPE = ‘B’ and i.author = ‘kim’  //싱글 테이블 전략에 해당 (다른 전략이라면 다른 쿼리 발생)

     

     

    조건식 (Case 식)

    기본 Case식

    select
      case when m.age <= 10 then '학생요금'
           when m.age >= 60 then '경로요금'
           else '일반요금'
      end
    from Member m
    String query = "select " +
                        "case when m.age <= 10 then '학생요금' " +
                        "     when m.age >= 60 then '경로요금' " +
                    "end " +
                    "from Member m";
    List<String> result = em.createQuery(query, String.class)
                    .getResultList();
    
    for (String s : result) {
        System.out.println("s = " + s);  //학생 요금
    }

     

    단순 CASE 식

    select
      case t.name
         when '팀A' then '인센티브110%'
         when '팀B' then '인센티브120%'
         else '인센티브105%'
      end
    from Team t

     

    COALESCE

    • null일 경우 지정한 값을 반환합니다.
    //회원 이름이 없다면 '이름 없는 회원'을 반환
    select coalesce(m.username,'이름 없는 회원') from Member m

     

    NULLIF

    • 지정한 두 값이 같으면 null 반환을 반환하고, 다르면 첫번째 지정한 값을 반환합니다.
    //회원 이름이 '관리자'라면 null을 반환하고, 아니라면 m.username 반환
    select NULLIF(m.username, '관리자') from Member m

     

     

    Named 쿼리

    • 미리 정의해서 이름을 부여해두고 사용하는 JPQL입니다. 
    • 정적 쿼리만 가능합니다. 
      • 동적으로 문자 더하기하여 생성할 수 없습니다. 
    • 어노테이션, XML에 정의합니다. 
    • 장점: 애플리케이션 로딩 시점에 SQL로 변환하여 캐시하여 재사용합니다. → 미리 변환하여 캐시해놓으므로 SQL로 변환하는 비용이 줄어듭니다. 
    • 장점: 애플리케이션 로딩 시점에 쿼리를 검증합니다.

     

    어노테이션

    @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();

    XML에 정의

    [META-INF/persistence.xml]

    <persistence-unit name="jpabook" >
    	 <mapping-file>META-INF/ormMember.xml</mapping-file>

    [META-INF/ormMember.xml]

    <?xml version="1.0" encoding="UTF-8"?>
    <entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
    
      <named-query name="Member.findByUsername">
         <query><![CDATA[
             select m
             from Member m
             where m.username = :username
         ]]></query>
      </named-query>
    
      <named-query name="Member.count">
         <query>select count(m) from Member m</query>
      </named-query>
    
    </entity-mappings>

     

    Named 쿼리 환경에 따른 설정

    • XML이 항상 우선권을 가집니다. 
    • 애플리케이션 운영 환경에 따라 다른 SQL을 사용하고 싶다면, 다른 XML을 배포하면 됩니다.

     

    참고 스프링 데이터 JPA에서 네임드 쿼리를 간편하게 사용할 수 있습니다.
    public interface UserRepository extends JpaRepository<User, Long> {
    	@Query("select u from User u where u.emailAddress = ?1"
    	User findByEmailAddress(String emailAddress);
    }​

     

    기본 함수

    • CONCAT (문자 더함)
    String query = "select concat('a', 'b') from Member";
    //String query = "select 'a' || 'b' from Member"  //하이버네이트 제공, 위의 동일한 기능
    ---
    s = ab
    • SUBSTRING
    String query = "select substring(m.username, 2, 3) from Member";
    • TRIM
    • LOWER, UPPER
    • LENGTH
    • LOCATE (인덱스 찾음)
    String query = "select locate('de', 'abcdegf') from Member";
    ---
    4
    • ABS, SQRT, MOD
    • SIZE (컬렉션의 사이즈), INDEX(JPA 용도)
    String query = "select size(t.members) from Team t";
    ---
    2

     

     

    사용자 정의 함수

    • JPQL에서 사용자 정의 함수를 사용할 수 있습니다. 
    • 하이버네이트에서는 사용하는 DB 방언을 상속받아 정의하고, 사용자 정의 함수를 등록합니다.
    select function('group_concat', m.name) from Member m

     

    실습

    • DB 방언을 상속받아 사용자 정의 방언 생성
    • 사용자 정의 함수 등록
    package hellojpa.dialect;
    
    ...
    
    public class MyH2Dialect extends H2Dialect {
        public MyH2Dialect() {
            registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
        }
    }
    • 방언 변경
    <property name="hibernate.dialect" value="org.hibernate.dialect.MyH2Dialect"/>

     

    String query = "select function('group_concat', m.username) from Member  m";
    //String query = "select group_concat(m.username) from Member m");  //위와 동일
    List<String> result = em.createQuery(query, String.class)
                    .getResultList();
    
    ---
    s = member1, member2

     

    'Backend > JPA' 카테고리의 다른 글

    [JPQL] 조인  (0) 2023.12.24
    [JPQL] 프로젝션  (0) 2023.12.24
    [JPA] JPA의 다양한 쿼리 방법 소개  (0) 2023.12.24
    [JPA] 값 타입  (0) 2023.12.20
    [JPA] 프록시와 연관관계 관리  (0) 2023.12.20