본문 바로가기
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