데이터는 데이터베이스에 있으므로 SQL 로 내용을 최대한 걸러서 조회해야 한다. 하지만 ORM 을 사용하면 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요하다. 이를 해결하기 위한것이 JPQL
이다.
JPA가 공식지원하는 기능은 아래와 같다
JPA가 공식지원하지는 않지만 알아둘 가치가 있는 기능
JPQL은 SQL보다 간결하다
String jpql = "select m from Member as m where m.username = "kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
// 실행한 JPQL
select m
from Member as m
where m.username = 'kim'
// 실행된 SQL
select
member.id as id,
member.age as age,
member.team_id as team,
member.name as name
from
Member member
where
member.name = 'kim'
Criteria는 JPQL을 생성하는 빌더 클래스다. 문자가 아닌 query.select(m).where .... 처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 장점이 있다.
JPQL에서는 오타가 있어도 컴파일이 성공하며 서버에 배포가능하다. 해당 쿼리가 실행되는 런타임 시점에 오류가 발생한다. 그에반해 Criteria는 문자가 아닌 코드로 JPQL을 작성한다. 따라서 컴파일 시점에 오류를 발견할 수 있다.
장점
단점
Criteria 처럼 JPQL 빌더 역할을 한다. 코드기반이며 단순하고 사용하기 쉽다.
🔊 QueryDSL 은 JPA 표준이 아닌 오픈소스 프로젝트이다.우리가 프로젝트에 사용하고 있는 것도 이것 !
SQL을 직접 사용할 수 있는 기능을 지원하는 것이 Native SQL이다.
JPQL을 사용해도 가끔은 특정 데이터베이스에 의존하는 기능을 사용할 때 사용한다. 단점은 특정 데이터베이스에 의존하는 SQL을 작성해야 하는 것. 즉, 데이터베이스를 변경하면 코드도 변경되어야 한다.
// JDBC Connection을 획득하는 방법
Session session = entityManager.unwrap(Session.class);
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
// work...
}
});
JDBC나 마이바티스를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시 해야함. 위 방법들은 JPA를 우회해야 하는데 이렇게 되면 JPA가 전혀 인식을 하지 못하는 문제가 발생한다. 즉, 데이터 무결성을 훼손할 수 있다는 이야기이다.
이런 이슈를 해결하는 방법은 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화하면 해결된다.
Spring framework를 사용하면 JPA와 마이바티스를 손쉽게 통합할 수 있다. 또한 AOP를 적절히 활용해서 JPA를 우회하여 데이터베이스에 접근하는 메소드를 호출할 때마다 영속성 컨텍스트를 플러시하면 위에서 언급한 문제도 깔끔하게 해결 가능하다.
JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있음.
EntityManager.persist() 메소드를 사용하면 되므로 INSERT문이 없다.
SELECT문
@Entity(name = "xxx")
로 지정 가능default
는 기본 클래스 명TypeQuery, query
작성한 JQPL을 실행하려면 쿼리 객체를 만들어야 한다.
TypeQuery
객체를 사용Query
객체를 사용// TypeQuery
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
// Query
Query query = em.createQuery("select m from Member m");
타입을 변환할 필요가 없는 TypeQuery를 사용하는 것이 더 편리
결과 조회
javax.persistence.NoResultException
발생javax.persistence.NonUniqueResultException
발생이름기준 파라미터
TypedQuery<Member> query =
em.createQuery("select m from Member m where m.username = :username"
, Member.class);
String usernameParam = "jong9";
query.setParameter("username", usernameParam);
위치 기준 파라미터
List<Member> members =
em.createQuery("select m from Member m where m.username = ?1"
, Member.class)
.setParameter(1, usernameParam)
.getResultList();
위치 기준 파라미터보다 이름기준 파라미터 바인딩 방식이 더 명확함
SELECT 절에 조회할 대상을 지정하는 것
프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등)이 있다.
엔티티 프로젝션
SELECT m from Member m // 회원
SELECT m.team FROM Member m // 팀
임베디드 타입 프로젝션
여러 값 조회
List<Object[]> resultList = em.createQuery("SELECT o.member, o.product,
o.orderAmount FROM Order o").getResultList();
for(Object[] row : resultList) {
Member member = (Member) row[0];
Product product = (Product) row[1];
int orderAmount = (Integer) row[2];
}
이 때 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
NEW 명령어
TypedQuery<UserDTO> query =
em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age)
FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
데이터베이스마다 페이징을 처리하는 SQL 문법이 다르다
JPA는 페이징을 두 API로 추상화하였다
TypedQuery<Member> query =
em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);
query.setFirstResult(10);
query.setMaxResults(20);
query.getResultList();
// FirstResult의 시작은 10이므로 11번째부터 시작해서 총 20건의 데이터를 조회
// 따라서 11~30 데이터를 조회
데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언 덕분이다.
ex)
// HSQLDB
ORDER BY M.NAME DESC OFFSET ? LIMIT ?
// MySQL
ORDER BY M.NAME DESC OFFSET LIMIT ?, ?
// PostgreSQL
ORDER BY M.NAME DESC LIMIT ? OFFSET ?
데이터베이스마다 SQL이 다를 뿐 아니라 오라클과 SQLServer는 페이징 쿼리를 따로 공부해야한다.
페이징 SQL을 최적화하고 싶다면 JPA가 제공하는 페이징 API가 아닌 네이티브 SQL을 직접 사용해야한다..
집합은 집합함수와 함께 통계 정보를 구할 때 사용
집합함수 사용 시 참고사항
GROUP BY, HAVING
정렬(ORDER BY)
SQL조인과 기능은 같고 문법만 약간 다르다
내부 조인
JPQL 내부 조인과 SQL 조인의 가장 큰 차이점은 아래와 같이 연관 필드를 사용한다는 것이다.
FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID
기존 SQL 처럼 아래와 같이 사용하면 오류가 발생한다.
FROM MEMBER m JOIN TEAM t
서로 다른 타입의 두 엔티티를 조회했으므로 TypeQuery를 사용할 수 없다
외부 조인
JPQL의 외부 조인은 기능상 SQL의 외부 조인과 같다. OUTER는 생략가능해서 보통 LEFT JOIN으로 사용한다.
컬렉션 조인
일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.
세타 조인
WHERE절을 사용해서 세타 조인 할 수 있다. 세타 조인은 내부 조인만 지원한다.
// JPQL
SELECT COUNT(m) FROM MEMBER m, TEAM t
WHERE m.username = t.name
// SQL
SELECT COUNT(m.id)
FROM
MEMBER M CROSS JOIN TEAM t
WHERE
m.username = t.name
JOIN ON 절
JPA 2.1부터 조인할 때 ON절을 지원한다. ON절을 사용하면 조인 대상을 필터링하고 조인할 수 있다. 내부조인의 ON 절은 WHERE 절을 사용할 때와 결과가 같으므로 보통 ON절은 외부 조인에서만 사용한다.
페치 조인
Fetch 조인은 SQL의 조인 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이며 join fetch 명령어로 사용할 수 있다.
엔티티 페치조인
SELECT m FROM MEMBER m JOIN FETCH m.team
이렇게 하면 회원과 팀을 함께 조회한다. 일반적인 JPQL 조인과는 다르게 fetch join은 별칭을 사용할 수 없다. but, 하이버네이트는 fetch join에도 별칭을 허용한다.
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();
for (Member member : members) {
System.out.println("username =" + member.getUsername() +", " +
"teamname = " + member.getTeam().name());
}
// 출력결과
username = 회원 1, teamname = 팀 A
username = 회원 2, teamname = 팀 A
username = 회원 3, teamname = 팀 B
연관된 팀을 사용해도 지연로딩이 일어나지 않는다. 또한 프록시가 아닌 실제 엔티티이므로 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.
컬렉션 페치조인
String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
for(Team team : teams) {
System.out.println("teamname = " + team.getName() + ",
team = " + team);
for (Member member : team.getMembers()) {
System.out.println(
"->username =" + member.getUsername() + ",
member = " + member);
}
}
// 페치조인으로 팀과 회원을 함께 조회해서 지연로딩 발생 안함
출력결과 팀A가 중복 조회되는 문제가 발생한다.
페치조인과 DISTINCT
중복된 결과를 제거하는 명령어이다. JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론이고 애플리케이션에서 한번 더 중복을 제거한다.
select distinct t
from Team t join fetch t.members
where t.name = '팀A'
각 Row의 결과 데이터가 다르므로 SQL의 DISTINCT는 효과가 없다.
ex) team A, 회원1 / team A, 회원2 → 팀은 중복되나 회원이 다르기에 중복제거가 안됨
이후 애플리케이션에서 DISTINCT 명령어를 보고 Team의 중복만을 제거한다.
페치조인과 일반조인의 차이
JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다. 즉시로딩으로 설정하면 회원 컬렉션을 즉시로딩하기위해 쿼리를 한번 더 실행한다.
반면 페치조인을 사용하면 연관된 엔티티도 함께 조회한다.
페치조인의 특징과 한계
글로벌 로딩 전략을 즉시로딩으로 설정하면 성능에 악영향을 미칠 수 있으니 되도록이면 지연 로딩을 사용하고 최적화가 필요한 곳에만 페치조인을 적용하는게 효과적이다.
페치조인을 사용하면 연관된 엔티티를 쿼리시점에 조회하기 때문에 지연로딩 미발생, 준영속 상태에서도 객체 그래프를 탐색할 수 있다.
하지만 한계 또한 존재한다
쉽게 이야기해서 점을 찍어 객체 그래프를 탐색하는 것
ex) t.name
경로 표현식 용어 정리
경로 표현식과 특징
SELECT m.username, m.age FROM Member m;
SELECT m.* FROM Orders o
INNER JOIN Member m ON o.member_id = m.id;
단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부조인이 일어나는데 이것을 묵시조인 이라 한다. 묵시적 조인은 모두 내부 조인이다.SELECT t.members FROM Team t; // 성공
SELECT t.members.username FROM Team t; // 실패
컬렉션 값에서 경로 탐색을 시도하지 말자 !!경로 탐색을 사용한 묵시적 조인 시 주의사항
JPQL 도 서브쿼리를 지원하지만 몇가지 제약이 있는데, 서브쿼리를 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용할 수 없다.
SELECT m FROM Member m WHERE m.age > (SELECT AVG(m2.age) FROM Member m2)
연산자 우선 순위
Between, IN, Like, NULL 비교
컬렉션 식
// 예제
SELECT t FROM Team t WHERE :memberParam Member OF t.members
스칼라 식
CASE 식
특정 조건에 따라 분기할 때 CASE식 사용
CASE
{WHEN 조건식 THEN 스칼라식} +
ELSE 스칼라식
END
CASE 조건대상
{WHEN 스칼라식1 THEN 스칼라식2} +
ELSE 스칼라식
END
COALESCE( 스칼라식 {, 스칼라식} +)
SELECT COALESCE( m.username, '이름없는 회원') FROM Member m
NULLIF(스칼라식, 스칼라식)
SELECT NULLIF(m.username, '관리자') FROM Member m
JPQL로 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회한다.
// 단일 테이블 전략 사용시 실행되는 SQL
SELECT * FROM ITEM
// 조인 전략 사용시 실행되는 SQL
SELECT
*
FROM
Item i
LEFT OUTER JOIN
Book b ON ~~~
LEFT OUTER JOIN
Album a ON ~~~
LEFT OUTER JOIN
Movie m ON ~~~
TYPE
엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용
// JPQL
SELECT i FROM Item i
WHERE TYPE(i) IN (Book, Movie)
// SQL
SELECT i FROM Item i
WHERE i.DTYPE IN ('B', 'M')
TREAT (JPA 2.1)
자바의 타입 캐스팅과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용.
// JPQL
SELECT i FROM Item i WHERE TREAT(i as Book).author = 'kim'
// SQL
SELECT i.* FROM Item i
WHERE
i.DTYPE = 'B'
AND i.author = 'kim'
TREAT을 사용하여 부모타입인 item을 자식타입인 Book으로 다룬다. 따라서 author 필드에 접근 가능
FUNCTION_INVOCATION:: = FUNCTION(FUNCTION_NAME {, FUNCTION_ARG}*)
SELECT FUNCTION('GROUP_CONCAT', i.name) FROM Item i
하이버네이트 구현체를 사용하면 방언 클래스를 상속해서 구현하고 사용할 데이터베이스 함수를 미리 등록해야 한다
public class MyH2Dialect extends H2Dialect {
public MyH2Dialect() {
registerFunction( "group_concat", new StandardSQLFunction
("group_concat", StandardBasicTypes.STRING));
}
}
<property name = "hibernate.dialect" value = "hello.MyH2Dialect" />
하이버네이트 구현체를 사용하면 아래와 같이 축약해서 사용 가능하다
SELECT GROUP_CONCAT(i.name) FROM Item i
SELECT COUNT(m.id) FROM Member m
SELECT COUNT(m) FROM Member m
// 위 결과 값은 동일한 SQL을 실행한다
String qlString = "select m from Member m where m.team.id = :teamId";
List resultList = em.createQuery(qlString)
.setParameter("teamId", 1L)
.getResultList();
예제에서 m.team.id 를 보면 Member와 Team간의 묵시적 조인이 일어날 것 같지만 Member 테이블이 team_id 외래키를 갖고 있어 일어나지 않는다. 따라서 m.team 을 사용하든 m.team.id 를 사용하든 생성되는 SQL은 같다.JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있다.
Named 쿼리 장점
@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", "회원")
.getResultList();
// 위와 같이 Named쿼리 이름을 createNamedQuery 메소드 안에 넣어주면 된다
🔊 findByUsername이 아닌 Member.findByUsername 으로 한 이유는
영속성 유닛 단위로 관리되므로 충돌을 방지하기 위함이다.
또한 엔티티명이 앞에있으면 관리하기 쉽다 !
하나의 엔티티에 여러개의 쿼리를 넣고싶다면
@Entity
@NamedQueries({
@NamedQuery (
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"),
@NamedQuery (
name = "Member.count",
query = "select count(m) from Member m")
})
public class Member {
...
}
쿼리 어노테이션의 내부구조이다
@Target({TYPE})
public @interface NamedQuery {
String name(); // 쿼리 이름 (required = true)
String query(); // JPQL 정의 (required = true)
LockModeType lockMode() default NONE; // 쿼리 실행시 락모드 설정
QueryHint[] hints() default{}; // JPA 구현체에 힌트를 줄 수 있음
}
Named 쿼리를 XML에 정의
네임드 쿼리를 작성할 때는 XML을 사용하는 것이 더 편리하다.
또한 XML과 어노테이션에 같은 설정이 있으면 XML이 우선권을 가진다.
// JPQL : select m from Member m
// 쿼리 빌더
CriteriaBuilder cb = em.getCriteriaBuilder();
// 생성, 반환타입 지정
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
// From절
Root<Member> m = cq.from(Member.class);
// 검색 조건
PRedicate usernameEqual = cb.equal(m.get("username"), "회원");
//정렬 조건 정의
javax.persistence.criteria.Order ageDesc = cb.ddesc(m.get("age"));
// 쿼리 생성
cq.select(m)
.where(usernameEqual)
.orderBy(ageDesc);
TypedQuery<Member> query = em.createQuery(cq);
List<Member> members = query.getResultList();
public interface CriteriaBuilder {
CriteriaQuery<Object> createQuery();
<T> CriteriaQuery<T> createQuery(Class<T> resultClass);
CriteriaQuery<Tuple> createTupleQuery();
...
}
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> cq = cb.createQuery();
...
List<Object[]> resultList = em.createQuery(cq).getResultList();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createQuery();
...
List<Tuple> resultList = em.createQuery(cq);
// JPQL : select m.username, m.age
cq.multiselect(m.get("username"), m.get("age"));
CriteriaBuilder cb = em.getCriteriaBuilder();
cq.select(cb.array(m.get("username"), m.get("age"));
DISTINCT
select, multiselect 다음에 distinct(true)를 사용하면 된다
// JPQL : select distinct m.username, m.age
cq.multiselect(m.get("username"), m.get("age")).distinct(true);
NEW.construct()
JPQL에서 select new 생성자 구문을 Criteria 에서는 cb.construct(클래스타입, ...) 로 사용한다.
cq.select(cb.construct(MemberDTO.class, m.get("username"), m.get("age")));
튜플
Criteria는 Map과 비슷한 튜플이라는 특별한 반환 객체를 제공
TypedQuery<Tuple> query = em.createQuery(cq);
List<Tuple> resultList = query.getResultList();
for (Tuple tuple : resultList) {
String username = tuple.get("username", String.class);
...
}
Group By
cq.groupBy(m.get("team").get("name")); // Group By
// = group by m.team.name
Having
having(cb.gt(minAge, 10))
// = having min(m.age) > 10
cb.desc (...) 또는 cb.asc(...)
join() 메소드와 JoinType 클래스 사용
public enum JoinType {
INNER,
LEFT,
RIGHT
// JPA 구현체나 데이터베이스에 따라 지원하지 않을 수 있다.
}
// Join
m.join("team"); // 내부
m.join("team", JoinType.INNER); // 내부
m.join("team", JoinType.LEFT); // 외부
// Fetch Join
m.fetch("team", JoinType.LEFT); // 페치
간단한 서브쿼리
/* JPQL :
select m from Member m
where m.age >=
(select AVG(m2.age) from Member m2)
*/
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> mainQuery = cb.createQuery(Member.class);
// 서브쿼리
SubQuery<Double> subQuery = mainQuery.subQuery(Double.class);
Root<Member> m2 = subQuery.from(Member.class);
subQuery.select(cb.avg(m2.<Integer>get("age")));
// 메인쿼리
Root<Member> m = mainQuery.from(Member.class);
mainQuery.select(m)
.where(cb.ge(m.<Integer>get("age"), subQuery));
상호 관련 서브 쿼리
/* JPQL :
select m from Member m
where exists
(select t from m.team t where t.name = '팀A')
*/
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> mainQuery = cb.createQuery(Member.class);
Root<Member> m = mainQuery.from(Member.class);
SubQuery<Team> subQuery = mainQuery.subQuery(Team.class);
Root<Member> subM = subQuery.correlate(m); // 메인 쿼리의 별칭을 가져옴
Join<Member, Team> t = subM.join("team");
subQuery.select(t)
.where(cb.equal(t.get("name"), "팀A"));
// 메인 쿼리 생성
mainQuery.select(m)
.where(cb.exists(subQuery));
List<Member> resultList = em.createQuery(mainQuery).getResultList();
/* JPQL
select m from Member m
where m.username in ("회원1", "회원2")
*/
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class);
cq.select(m)
.where(cb.in(m.get("username"))
.value("회원1")
.value("회원2"));
/* JPQL
select m.username,
case when m.age>=60 then 600
when m.age<=15 then 500
else 1000
end
from Member m
*/
cq.multiselect(
m.get("username"),
cb.selectCase()
.when(cb.ge(m.<Integer>get("age"), 60), 600)
.when(cb.le(m.<Integer>get("age"), 15), 500)
.otherwise(1000)
);
/* JPQL
select m from Member m
where m.username = :usernameParam
*/
cq.select(m)
.where(cb.equal(m.get("username"), cb.parameter(String.class, "usernameParam")));
List<Member> resultList = em.createQuery(cq)
.setParameter("usernameParam", "회원1")
.getResultList();
Expression<Long> function = cb.function("SUM", Long.class, m.get("age"));
cq.select(function);
JPQL 동적 쿼리는 공백을 입력하지 않거나 where와 and의 위치 구성을 신경쓰지 않으면 에러를 접할 수 있어 불편하다.
Integer age =10;
if(age != numm) criteria.add(cb.equal(m.<Integer>get("age"),
cb.parameter(Integer.class, "age")));
cq. where(cb.and(criteria.toArray(new Predicate[0])));
if(age != null) query.setParameter("age", age);
위와 같이 Criteria 동적 쿼리는 where, and의 위치나 공백으로 인해 에러는 없지만 장황하고 복잡하다.