Java Persistence Query Language
SQL을 추상화하여, 테이블(레코드) 단위가 아닌 Entity 객체 대상으로 DB에서 데이터를 다루도록 한다.
SQL을 추상화했기 때문에 특정 DBMS의 SQL에 종속적이지 않다.
DBMS는 SQL만 이해할 수 있으므로, 특정 시점에 JPQL은 SQL로 번역된다.
JPQL은 결국 스크립트, 즉 문자열이다. 그렇다보니 동적 쿼리를 만들기가 어렵다. (버그 발생 가능성)
이를 개선하기 위해 Criteria
를 사용하기도 하나, 유지보수가 어렵다는 단점 때문에 잘 사용하지 않는다.
그대신 QueryDSL
을 주로 사용한다.
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
는 쿼리 결과가 여러 건일 때 사용한다.
List<Member> members = em.creatQuery("...").getResultList();
쿼리 결과가 없을 경우에는 빈 리스트를 반환한다.
getSingleResult
는 쿼리 결과가 1개인 것이 명확할 때 사용한다.
Member member = em.createQuery("PK로 조회").getSingleResult();
만약 결과값이 없으면 NoResultException
, 결과값이 2개 이상이면 NonUniqueResultException
이 발생한다.
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;
하나씩 특징을 살펴보자
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 ...;
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
);
TypedQuery<Team> teams = em.createQuery(
"SELECT m.address FROM Member AS m",
Team.class
);
Embedded 타입을 불러올 수 있다. Embedded 타입은 자신이 포함된 Entity의 Table에 컬럼으로 직접 들어가 있으므로, Join이 발생하지 않는다.
Query teams = em.createQuery(
"SELECT m.name, m.age FROM Member AS m"
);
Entity, Embedded Type이 아닌 Scalar Type은, 조회할 값이 여러가지 인 경우 특정 타입으로 한정(?) 할 수 없다. 그러므로 다음 방법들으로 값을 조회할 수 있다.
위에서 설명한 특정 타입을 명시할 수 없는 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
별로 특별할 것 없이, Object[]
를 반환 타입으로 명시해주자
TypedQuery<Object[]> query = em.createQuery(
"SELECT u.name, u.id FROM User AS u",
Object[].class
);
List<Object[]> results = query.getResultList();
조회할 컬럼들을 가지고 있는 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();
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
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
// 팀이름이 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';
// 모든 제품의 제고수량보다 주문개수가 많은 주문들 조회
SELECT o FROM Order AS o
WHERE o.orderAmount > ALL (SELECT p.stockAmount FROM Product AS p);
// 어떤 팀이든 팀에 속한 멤버 조회
SELECT m from Member AS m
WHERE m.team = ANY (SELECT t from Team AS t);
// 자신이 속한 팀의 이름과, 어떤 팀이든 이름이 같은 멤버 조회
SELECT m FROM Meber AS m
WHERE m.team IN (SELECT t.name FROM Team AS t);
JPA 표준 스팩은 WHERE, HAVING 절 서브 쿼리만 지원한다.
(Hibernate는 SELECT 서브 쿼리도 지원한다.)
FROM절 서브 쿼리는 JPQL에서 불가능하다.
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();
em.createQuery(
"SELECT
CASE WHEN m.age <= 10 THEN '학생요금'
WHEN m.age >= 60 THEN '경로요금'
ELSE '일반요금'
END
FROM Member AS m
"
)
em.createQuery(
"SELECT
CASE t.name
WHEN 'a' then 'aa'
WHEN 'b' then 'bb'
ELSE 'cc'
END
FROM Member AS m
"
)
하나씩 조회해서 null이 아니면 반환한다.
SELECT coalesce(m.name, 'not name member') FROM Member AS m
두 값이 같으면 null 반환, 다르면 첫번째 값을 반환한다.
SELECT NULLIF(m.name, 'admin') FROM Member AS m;
concat
substirng
trim
lower, upper
length
locate
abs, sqrt, mod
size(Collection 크기 반환)
다양한 함수를 제공한다.
사용자가 정의한 함수를 JPQL에서 사용할 수 있다.
다음에 정리,,,