JPQL 기초

dev_314·2023년 3월 10일
0

JPA - Trial and Error

목록 보기
9/16

JPQL

Java Persistence Query Language

SQL을 추상화하여, 테이블(레코드) 단위가 아닌 Entity 객체 대상으로 DB에서 데이터를 다루도록 한다.
SQL을 추상화했기 때문에 특정 DBMS의 SQL에 종속적이지 않다.

DBMS는 SQL만 이해할 수 있으므로, 특정 시점에 JPQL은 SQL로 번역된다.

참고: QueryDSL

JPQL은 결국 스크립트, 즉 문자열이다. 그렇다보니 동적 쿼리를 만들기가 어렵다. (버그 발생 가능성)

이를 개선하기 위해 Criteria를 사용하기도 하나, 유지보수가 어렵다는 단점 때문에 잘 사용하지 않는다.

그대신 QueryDSL을 주로 사용한다.

TypedQuery, Query

TypedQuery는 반환값의 타입이 명확할 때 사용한다.

TypedQuery<String> typedQuery = em.createQuery(
	"SELECT m FROM Member AS m", 
    Member.class // Entity
);

TypedQuery<String> typedQuery = em.createQuery(
	"SELECT m.name FROM Member AS m WHERE m.name LIKE ...", 
    String.class
);

Query는 반환값이 타입이 불분명할 때 사용한다.

Query query = em.createQuery(
	"SELECT m.name, m.age FROM Member as m"
    // name은 String, age는 int
);

getResultList(), getSingleResult()

getResultList는 쿼리 결과가 여러 건일 때 사용한다.

List<Member> members = em.creatQuery("...").getResultList();

쿼리 결과가 없을 경우에는 빈 리스트를 반환한다.

getSingleResult는 쿼리 결과가 1개인 것이 명확할 때 사용한다.

Member member = em.createQuery("PK로 조회").getSingleResult();

만약 결과값이 없으면 NoResultException, 결과값이 2개 이상이면 NonUniqueResultException이 발생한다.

setParameter

setParameter를 통해 파라미터를 설정할 수 있다.

TypedQuery query = em.createQuery(
	"SELECT m FROM Member AS m WHERE m.name = :username",
    Member.class
);
query.setParameter("username", "username#1");

// 메서드 체이닝을 사용한 개선
TypedQuery query = em.createQuery(
	"SELECT m FROM Member AS m WHERE m.name = :username",
    Member.class
).setParameter("username", "username#1");

위 방식 처럼 파라미터 이름 기준으로 바인딩을 할 수 있고, 아래처럼 파라미터 순서(위치) 기준으로 바인딩 할 수도 있다 (사용 비권장)

TypedQuery query = em.createQuery(
	"SELECT m FROM Member AS m WHERE m.name = ?1",
    Member.class
).setParameter(1, "username#1");

프로젝션

프로젝션은 SELECT절에 조회할 대상을 지정하는 것을 의미한다.

다양한 데이터 타입을 프로젝션 할 수 있다.

// Entity
SELECT m FROM Member AS m;
// Entity (연관관계)
SELECT m.team FROM Member AS m;
// Embedded Type
SELECT m.address FROM Member AS m;
// Scalar Type
SELECT m.username, m.age FROM Member AS m;

하나씩 특징을 살펴보자

Entity

List<Member> members = em.createQuery(
	"SELECT m FROM Member AS m",
    Member.class
).getResultList();

쿼리 결과로 불러온 Entity 객체들은 모두 Persistence Context에 올라간다.

따라서 아래와 같이 값을 set하면 Update 쿼리가 발생한다.

members.get(0).setAge(10);
// UPDATE Member SET age = 10 WHERE ...;

Entity + Join

TypedQuery<Team> teams = em.createQuery(
	"SELECT m.team FROM Member AS m",
    Team.class
);

Member Entity를 통해 Team Entity를 불러올 수 있다. 이 과정에서 Join문이 발생하는데, JPQL는 Join이 드러나있지 않다(이런 경우를 묵시적 Join이라고 한다). 이는 유지보수와 쿼리 예측이 힘들다는 문제가 발생한다.

따라서 위와 같은 경우에는 명시적으로 Join문을 사용하자.

TypedQuery<Team> teams = em.createQuery(
	"SELECT t FROM Member AS m JOIN Team as t",
    Team.class
);

Embedded Type

TypedQuery<Team> teams = em.createQuery(
	"SELECT m.address FROM Member AS m",
    Team.class
);

Embedded 타입을 불러올 수 있다. Embedded 타입은 자신이 포함된 Entity의 Table에 컬럼으로 직접 들어가 있으므로, Join이 발생하지 않는다.

Scalar Type

Query teams = em.createQuery(
	"SELECT m.name, m.age FROM Member AS m"
);

Entity, Embedded Type이 아닌 Scalar Type은, 조회할 값이 여러가지 인 경우 특정 타입으로 한정(?) 할 수 없다. 그러므로 다음 방법들으로 값을 조회할 수 있다.

Query 사용

위에서 설명한 특정 타입을 명시할 수 없는 Query를 사용할 수 있다.

Query query = em.creatQuery(
	"SELECT m.name, m.age FROM Member AS m"
);
// Object[][] 형태의 이차원 배열로 생각하면 쉽다.
Object[] results = query.getResultList();

// 첫 번째 결과
Object[] result = (Object[]) results.get(0); 
sout(result[0]); // name
sout(result[1]); // age

TypedQuery 사용

별로 특별할 것 없이, Object[]를 반환 타입으로 명시해주자

TypedQuery<Object[]> query = em.createQuery(
	"SELECT u.name, u.id FROM User AS u",
    Object[].class
);
List<Object[]> results = query.getResultList();

DTO 사용

조회할 컬럼들을 가지고 있는 DTO를 사용하자.

@Getter
@AllArgsConstructor
class MemberInfoDTO {
	private String name;
    private int age;
}

List<MemberInfoDTO> memberInfoDTOs = em.createQuery(
	"SELECT new MemberInfoDTO(m.name, m.age) FROM Member AS m", 
    MemberInfoDTO.class
).getResultList();

페이징 쿼리

복잡한 페이징 쿼리를 JPA는 2개의 메서드로 정리했다.

몇 번째 부터(setFirstResult), 몇 개 가져올래(setMaxResult)
List<Car> resultList = em.createQuery(
	"SELECT c FROM Car AS c ORDER BY c.id ASC", Car.class)
    .setFirstResult(0)
    .setMaxResults(10)
    .getResultList();

Join

내부 조인

SELECT m FROM Member AS m [INNER] JOIN m.team AS t

외부 조인

SELECT m FROM Member AS m [LEFT|RIGHT][OUTER] JOIN m.team AS t 

세타 조인

aka Cross Join

SELECT count(m) FROM Member AS m, Team AS t WHERE m.username = t.name

Join의 On절

JPA 2.1부터 Join에 On절을 지원한다.
이를 통해 두 가지 작업이 가능하다.

조인 대상 필터링

// team 이름이 A인 팀만 Member와 Join한다.
SELECT m, t FROM Member AS m LEFT JOIN m.team AS t ON t.name = 'A'

위 JPQL은 아래의 SQL과 같다.

SELECT m.*, t.* FROM Member AS m LEFT JOIN Team AS t
ON m.team_id = t.id AND t.name = 'A';

연관관계 없는 엔티티 외부 조인

위에서 다룬 외부 조인은 PK = FK인 관계만 다룬다.
그런데 ON절을 사용하면 아래처럼 연관관계가 없는 조인을 사용할 수 있다.

// 멤버이름과 팀 이름이 같은 관계만 Join
SELECT m, t FROM Member AS m LEFT JOIN Team AS T
ON m.name = t.name;

위 JPQL은 아래의 SQL과 같다.

SELECT m.*, t.* FROM Member AS m LEFT JOIN Team AS t
ON m.name = t.name;

서브 쿼리

JQPL도 서브 쿼리를 사용할 수 있다.
JQPL도 결국 SQL로 전환되므로, JPQL도 비상관 서브 쿼리상관 서브 쿼리보다 성능이 더 낫다.

// 비상관 쿼리
SELECT m FROM Member AS m
WHERE m.age > (SELECT avg(m2.age) FROM Member AS m2)

// 상관 쿼리
SELECT m FROM Member AS m
WHERE (SELECT count(0) FROM Order AS o WHERE m = o.member) > 0

서브 쿼리 지원 함수

EXIST

// 팀이름이 a인 팀에 소속된 팀원 조회
SELECT m FROM Member AS m
WHERE exists(SELECT t FROM m.team AS t where t.name = 'a')
// 서브 쿼리를 사용한 쿼리 발생

// 같은 결과지만 Join을 사용한 쿼리 발생
SELECT m FROM Member AS m WHERE m.team.name = 'a';

ALL

// 모든 제품의 제고수량보다 주문개수가 많은 주문들 조회
SELECT o FROM Order AS o
WHERE o.orderAmount > ALL (SELECT p.stockAmount FROM Product AS p);

ANY

// 어떤 팀이든 팀에 속한 멤버 조회
SELECT m from Member AS m
WHERE m.team = ANY (SELECT t from Team AS t);

IN

// 자신이 속한 팀의 이름과, 어떤 팀이든 이름이 같은 멤버 조회
SELECT m FROM Meber AS m
WHERE m.team IN (SELECT t.name FROM Team AS t);

JPA 서브 쿼리의 한계

  1. JPA 표준 스팩은 WHERE, HAVING 절 서브 쿼리만 지원한다.
    (Hibernate는 SELECT 서브 쿼리도 지원한다.)

  2. FROM절 서브 쿼리는 JPQL에서 불가능하다.

    1. Join으로 해결
    2. 쿼리를 여러번 날려서 Application에서 조작.
    3. Native Query

JPQL의 타입 표현

Enum

enum은 패키지명을 포함해서 사용해야 한다.

SELECT ...
WHERE u.type = UserType.ADMIN; // 잘못된 사용법

SELECT ...
WHERE u.type = app.user.enums.UserType.ADMIN; // 패키지명을 포함해야 한다.

상속 관계

상속 관계의 Entity는 type을 사용해서 조회 가능하다.

// Item class를 Book class가 상속받는 관계
List<Item> items = em.createQuery(
	"SELECT i FROM Item AS i WHERE type(i) = Book",
    Book.class
).getResultList();

조건식 (case)

기본 case

em.createQuery(
	"SELECT 
    	CASE WHEN m.age <= 10 THEN '학생요금'
        	WHEN m.age >= 60 THEN '경로요금'
            ELSE '일반요금'
        END
     FROM Member AS m
    "
)

단순 case

em.createQuery(
	"SELECT 
    	CASE t.name
        	WHEN 'a' then 'aa'
            WHEN 'b' then 'bb'
            ELSE 'cc'
        END
     FROM Member AS m
    "
)

조건 case

coalesce

하나씩 조회해서 null이 아니면 반환한다.

SELECT coalesce(m.name, 'not name member') FROM Member AS m

nullif

두 값이 같으면 null 반환, 다르면 첫번째 값을 반환한다.

SELECT NULLIF(m.name, 'admin') FROM Member AS m;

JPQL 함수

기본 함수

concat
substirng
trim
lower, upper
length
locate
abs, sqrt, mod
size(Collection 크기 반환)

다양한 함수를 제공한다.

사용자 정의 함수

사용자가 정의한 함수를 JPQL에서 사용할 수 있다.

다음에 정리,,,

profile
블로그 이전했습니다 https://dev314.tistory.com/

0개의 댓글