JPQL 기본 문법

뚝딱이·2022년 9월 18일
0

JPA

목록 보기
10/11

JPQL : Java Persistence Query Language

  • JPQL은 객체지향 쿼리 언어다.따라서 테이블을 대상으로 쿼리 하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
  • JPQL은 SQL을 추상화해서 특정데이터베이스 SQL에 의존하지 않는다.
  • JPQL은 결국 SQL로 변환된다

문법

select m from Member as m where m.age > 18 
  • 중요한 것은 위의 Member가 엔티티라는 것이다.
  • 엔티티와 속성은 대소문자 구분O (Member, age)
  • JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)
  • 엔티티 이름 사용, 테이블 이름이 아님(Member)
  • 별칭은 필수(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<Member> query1 = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class);
Query query3 = em.createQuery("select m.username, m.age from Member m");

query1에선 member를 조회하니 Member.class로 반환타입이 명확하다.
query2 또한 username을 반환하고, 이 username이 String형이므로 반환타입이 명확하다.
하지만 query3는 String 타입인 username과 int 타입인 age를 반환하므로 반환 타입이 명확하지 않다.
따라서 이렇듯 반환 타입이 명확할 때는 TypeQuery를 사용하고, 반환타입이 명확하지 않을 대는 Query를 사용한다.

결과 조회 API

  • query.getResultList():
    • 결과가 하나 이상일 때, 리스트 반환
    • 결과가 없으면 빈 리스트 반환
  • query.getSingleResult():
    • 결과가 정확히 하나, 단일 객체 반환
    • 결과가 없으면: javax.persistence.NoResultException
    • 둘 이상이면: javax.persistence.NonUniqueResultException

파라미터 바인딩

    TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = :username", Member.class);
            query.setParameter("username", "member1");

또는

Member result = em.createQuery("select m from Member m where m.username = :username", Member.class)
                    .setParameter("username", "member1")
                    .getSingleResult();

와 같이 사용한다.

위치 기반의 바인딩도 존재하는데 위치 기반 바인딩은 사용하지 않는 것이 좋다.

Member result = em.createQuery("select m from Member m where m.username = ?1", Member.class)
                    .setParameter(1, "member1")
                    .getSingleResult();

프로젝션

SELECT 절에 조회할 대상을 지정하는 것

  • 프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타
    입)
  • DISTINCT로 중복 제거
  • SELECT m FROM Member m -> 엔티티 프로젝션 : Member 엔티티 조회
List<Member> result = em.createQuery("select m from Member m", Member.class)
                    .getResultList();

위의 코드에서 반환된 result는 영속성 컨텍스트에서 관리가 될까 ? 엔티티 프로젝션을 하면 모두 영속성 컨텍스트에 관리된다.

  • SELECT m.team FROM Member m -> 엔티티 프로젝션 : Member의 team 조회
List<Team> result = em.createQuery("select m.team from Member m", Team.class)
                    .getResultList();

Team이랑 join하는 쿼리가 나간다.
하지만 JPQL은 최대한 SQL과 비슷하게 적는것이 좋다. 따라서 아래와 같이 하자.

List<Team> result = em.createQuery("select t from Member m join m.team t", Team.class)     

위와 같이 하면 join이 예상되기 때문에 join은 명시적으로 하는 것이 좋다.

  • SELECT m.address FROM Member m -> 임베디드 타입 프로젝션
List<Address> result = em.createQuery("select o.address from Order o", Address.class)
                    .getResultList();

Address가 소속되어있기 때문에 from Address가 될 수 없는 한계가 있다.

  • SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
em.createQuery("select m.username, m.age from Member m")
                    .getResultList();

근데 여기서 타입이 여러개인데 어떻게 조회할까 ?

1. Query 타입으로 조회

List resultList = em.createQuery("select m.username, m.age from Member m")
                    .getResultList();
            Object o = resultList.get(0);
            Object[] result = (Object[]) o;
            System.out.println("username = " + result[0]);
            System.out.println("age = " + result[1]);

2. Object[] 타입으로 조회

List<Order[]> resultList = em.createQuery("select m.username, m.age from Member m")
                    .getResultList();

            Order[] result = resultList.get(0);
            System.out.println("username = " + result[0]);
            System.out.println("age = " + result[1]);

3. new 명령어로 조회

  • 단순 값을 DTO로 바로 조회
    SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM
    Member m
  • 패키지 명을 포함한 전체 클래스 명 입력
  • 순서와 타입이 일치하는 생성자 필요
public class MemberDTO {
    private String username;
    private int age;

    public MemberDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
            List<MemberDTO> result = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
                    .getResultList();

            MemberDTO memberDTO = result.get(0);
            System.out.println("memberDTO.getUsername() = " + memberDTO.getUsername());
            System.out.println("memberDTO.getAge() = " + memberDTO.getAge());

https://inflearn.com/questions/13438

페이징

JPA는 페이징을 아래의 두 API로 추상화했다.

  • setFirstResult(int startPosition) : 조회 시작 위치
    (0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
                    .setFirstResult(1)
                    .setMaxResults(10)
                    .getResultList();

JPA는 이렇게 간편하다.

오라클을 사용한 쿼리를 살펴보자.

Hibernate: 
    /* select
        m 
    from
        Member m 
    order by
        m.age desc */ select
            * 
        from
            ( select
                row_.*,
                rownum rownum_ 
            from
                ( select
                    member0_.id as id1_0_,
                    member0_.age as age2_0_,
                    member0_.TEAM_ID as TEAM_ID4_0_,
                    member0_.username as username3_0_ 
                from
                    Member member0_ 
                order by
                    member0_.age desc ) row_ ) 
            where
                rownum_ <= ? 
                and rownum_ > ?

무려 select가 세번이다. 너무 복잡하다.
이렇듯 JPA를 사용하면 정말 간편하게 사용할 수 있다.

조인

  • 내부 조인:
    SELECT m FROM Member m [INNER] JOIN m.team t
    Member는 있고 Team은 없는 경우 데이터가 아예 안나온다.
String query = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(query, Member.class)
                    .getResultList();
  • 외부 조인:
    SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
    Member는 있고 Team은 없어도 Member 조회가능
String query = "select m from Member m left outer join m.team t";
List<Member> result = em.createQuery(query, Member.class)
                    .getResultList();
  • 세타 조인:
    select count(m) from Member m, Team t where m.username = t.name
    member와 team을 모두 꺼내서 비교
String query = "select m from Member m, Team t where m.username=team.name";
List<Member> result = em.createQuery(query, Member.class)
                    .getResultList();

ON절

  1. 조인 대상 필터링
    예) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
JPQL:
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' 
  1. 연관관계 없는 엔티티 외부 조인(하이버네이트 5.1부터)
    예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
JPQL:
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

https://inflearn.com/questions/34345
https://inflearn.com/questions/17488

서브 쿼리

  • 나이가 평균보다 많은 회원
    select m from Member m where m.age > (select avg(m2.age) from Member m2)
    메인 쿼리의 Member m 과 별개의 Member m2를 서브쿼리에서 사용

  • 한 건이라도 주문한 고객
    select m from Member m where (select count(o) from Order o where m = o.member) > 0

메인 쿼리의 Member m을 서브쿼리에서도 사용

지원 함수

  • [NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
팀A 소속인 회원
select m from Member m
where exists (select t from m.team t where t.name = ‘팀A') 
  • {ALL | ANY | SOME} (subquery)
    • ALL 모두 만족하면 참
    • ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
• 전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o 
where o.orderAmount > ALL (select p.stockAmount from Product p) 
• 어떤 팀이든 팀에 소속된 회원
select m from Member m 
where m.team = ANY (select t from Team t)
  • [NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참

한계

  • JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능
  • SELECT 절도 가능(하이버네이트에서 지원)
  • FROM 절의 서브 쿼리는 현재 JPQL에서 불가능
    • 조인으로 풀 수 있으면 풀어서 해결
    • 조인으로 못 풀면 쿼리를 둘로 분해해서 사용, 아니면 네이티브 쿼리 사용

https://inflearn.com/questions/48741

타입 표현

  • 문자: ‘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();
  • ENUM: jpabook.MemberType.Admin (패키지명 포함)
String query = "select m.username, 'HELLO', TRUE from Member m" + 
                    "where m.type = jpql.MemberType.ADMIN";
            
            List<Object[]> result = em.createQuery(query)
                    .getResultList();

위와 같이 패키지명을 다 적어줘야하지만, setParmater를 사용하면 패키지명까지 적어주진 않아도 된다.

String query = "select m.username, 'HELLO', TRUE from Member m" +
                    "where m.type = :userType";

List<Object[]> result = em.createQuery(query)
                    .setParameter("userType", MemberType.ADMIN)
                    .getResultList();
  • 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)
em.createQuery("select i from Item i where type(i) == Book ", Item.class);

Book이 Item을 상속받고 있을 때 위와 같이 사용할 수 있다.

기타

SQL과 문법이 같은 식

  • EXISTS, IN
  • AND, OR, NOT
  • =, >, >=, <, <=, <>
  • BETWEEN, LIKE, IS NULL

조건식

기본 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%'
    end
from Team t

조건과 정확하게 매칭 될 경우이다.

  • COALESCE: 하나씩 조회해서 null이 아니면 반환
//사용자 이름이 없으면 이름 없는 회원을 반환
String query = "select coalesce(m.username, '이름 없는 회원') from Member m";
  • NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
//사용자 이름이 '관리자'면 null을 반환하고 나머지는 본인의 이름을 반환
String query = "select nullif(m.username, '관리자') from Member m";

함수

JPQL 함수엔 세가지로 분류된다.
첫번째는 JPQL이 제공하는 표준함수이다. 따라서 DB에 관계없이 사용가능하다.
두번째는 사용자 정의함수이다. DB에 있는 함수를 불러서 사용할 경우
세번째는 DB가 등록해놓은 함수이다. 함수에 따라 다를 수 있기 때문에 함수 종속적이다.

기본 함수

  • CONCAT : 문자를 붙이는 것
String query = "select concat('a', 'b') from Member m";
  • SUBSTRING : 문자를 자를 때
String query = "select substring(m.username, 2, 3) from Member m";
  • TRIM : 공백 제거
  • LOWER, UPPER
  • LENGTH : 문자 길이
  • LOCATE : 해당 문자의 위치
String query = "select locate('de','abcdefg') from Member m";

• ABS, SQRT, MOD
• SIZE, INDEX(JPA 용도)

String query = "select size(t.members) from Team t";

size는 컬렉션의 크기를 반환한다.

값타입 컬렉션에서 위치를 알고 싶을 때 @OrderColumn을 사용하는데, 여기의 index도 이때 사용된다.

사용자 정의 함수 호출

  • 하이버네이트는 사용전 방언에 추가해야 한다.
    • 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
package dialect;

import org.hibernate.dialect.H2Dialect;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.type.StandardBasicTypes;

public class MyH2Dialect extends H2Dialect {
    public MyH2Dialect(){
        registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
    }
}

그리고 persistence에 사용하는 db를 H2Dialect가 아닌 MyH2Dialect로 바꾼다.

String query = "select function('group_concat',m.username) from Member m";

위와 같이 사용하면 된다.
group_concate은 데이터를 모두 한줄로 뽑아준다.

하이버네이트를 사용한다면

String query = "select group_concat(m.username) from Member m";

와 같이 사용할 수 있다.


출처 : 자바 ORM 표준 JPA 프로그래밍 - 기본편

profile
백엔드 개발자 지망생

0개의 댓글