10장 객체지향 쿼리 언어
10.1 객체지향 쿼리 소개
다음은 가장 단순한 검색방법이다.
- 식별자로 조회 :
EntityManager.find()
- 객체 그래프 탐색 L
(예:a.getB().getC())
ORM을 사용하면, 테이블 대상이 아닌 엔티티 개체를 대상으로 쿼리하는 방법이 필요하다.
엔티티 대상으로 쿼리하는 것을 JPQL(객체지향 SQL, 쿼리)
이라고 한다.
- 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- SQL : 데이터베이스 테이블을 대상으로 하는 쿼리
- JPQL : 엔티티 객체를 대상으로 하는 객체지향 쿼리
- JPQL을 사용하면 JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한다.
JPA는 JPQL뿐만 아니라 다양한 검색 방법을 제공하며
JPA가 공식 지원하는 기능은 다음과 같다.
- JPQL (Java Persistence Query Language)
- Criteria 쿼리 (Criteria Query) : JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
- 네이티브 SQL (Native SQL) : JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다.
JPA가 공식으로 지원하지 않는 검색 방법
- QueryDSL : Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스모음, 비표준 오픈소스 프레임워크
- JDBC, MyBatis(SQL 매퍼 프레임워크)
10.1.1 JPQL 소개
엔티티 객체를 조회하는 객체지향 쿼리이다.
- 문법은 SQL과 비슷하고 ANSI 표준 SQL이 제공하는 기능을 유사하게 지원한다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다.
- 데이터베이스 방언(Dialect)만 변경하면 JPQL을 수정하지 않아도 된다.
- JPQL은 SQL보다 간결하다. 엔티티 직접 조회, 묵시적 조인, 다형성 지원으로 SQL보다 코드가 간결하다.
@Entity(name="Member") // name 속성의 기본값은 클래스 명
public class Member {
@Column(name="name")
private String username;
//...
}
// 쿼리 생성
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
- JPQL을 사용하여 회원이름이 kim인 엔티티를 조회하는 방법이다.
- 조건절에 보면 m.username 은 테이블 컬럼명이 아니라 엔티티 객체의 필드명이다.
- em.createQuery() 메소드에 실행할 JPQL 구문과 반환할 엔티티 클래스 타입을 지정한다.
- 메소드를 실행하면 JPA는
JPQL을 SQL로 변환
해서데이터베이스를 조회
하고 결과값을 엔티티로 생성해서 반환한다.
10.1.2 Criteria 쿼리 소개
Criteria는 JPQL을 생성하는 빌더 클래스
이다.
- Criteria의 장점은 문자가 아닌 query.select(m).where(...)처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이다. -> 메서드 체이닝
- JPQL은 오타가 있어도 컴파일 및 빌드가 성공하지만 해당 쿼리가 실행되는 런타임 시점에 잡을 수 있다. 하지만 Criteria는 컴파일 시점에 오류를 알 수 있다.
즉, Criteria의 장점
은
- 컴파일 시점에 오류 발견하기 쉽다.
- IDE를 사용하면 코드 자동완성 지원
- 동적 쿼리를 작성하기 편함
// Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
// 루트 클래스(조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);
// 쿼리 실행
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));
List<Member> resultList = em.createQuery(cq).getResultList();
- 위의 예제를 보면
m.get("username")
을 보면 필드 명을 문자로 작성했다.- 조회 객체 부분도 코드로 사용하고 싶으면
메타 모델
을 사용하면 된다.
- 조회 객체 부분도 코드로 사용하고 싶으면
- 자바가 제공하는 어노테이션 프로세서 기능을 사용하면 어노테이션을 분석해서 클래스를 생성할 수 있다.
- JPA는 이 기능을 사용해서 Member 엔티티 클래스로부터
Member_
라는 Criteria 전용 클래스를 생성하는데 이것을 메타 모델이라 하며, 메타 모델의 자세한 설명은 뒤에서 하겠다.
- JPA는 이 기능을 사용해서 Member 엔티티 클래스로부터
// 메타 모델 사용전 -> 사용 후
m.get("username") => m.get(Member_.username)
장점
- 메타 모델을 사용함으로써 코드로 작성이 가능했으며 동적 쿼리를 작성할 때 유용하다.
단점
- 복잡하고 장황하다. 사용하기 불편하고 Criteria로 작성한 코드 가독성이 떨어진다는 단점이 있다.
10.1.3 QueryDSL 소개
QueryDSL도 Criteria처럼 JPQL 빌더 역할을 한다.
QueryDSL 장점은 코드 기반이면서 단순하고 사용하기 쉽다. 작성한 코드도 JPQL과 비슷해서 가독성이 좋다.
// 준비
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;
// 쿼리, 결과조회
List<Member> members = query.from(member)
.where(member.username.eq("kim"))
.list(member);
QMember
는 Member 엔티티 클래스를 기반으로 생성한QueryDSL 쿼리 전용 클래스
이다.
QueryDSL은 JPA 표준은 아니고 오픈소스 프로젝트다.
JDO, 몽고DB, 등도 지원한다.
10.1.4 네이티브 SQL 소개
- JPA는 SQL을 직접 사용할 수 있는 네이티브 SQL이라 한다.
- JPQL을 사용해도 가끔은 특정 데이터베이스에 의존하는 기능을 사용해야 할 때 사용한다. .
오라클 데이터베이스만 사용하는 CONNECT BY 기능이나 특정 데이터베이스에서만 동작하는 SQL 힌트 같은 것
- SQL은 지원하지만 JPQL이 지원하지 않는 기능도 있다. 이때 네이티브 SQL을 사용하면 해결이 가능하다.
String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();
- 네이티브 SQL 단점은 특정 데이터베이스에 의존하는 SQL을 작성해야 하며, 데이터베이스가 변경되면 쿼리문을 수정해야한다.
10.1.5 JDBC 직접 사용, 마이바티스 같은 SQL 매퍼 프레임워크 사용
- JDBC 커넥션에 직접 접근하고 싶으면 JPA는 JDBC 커넥션을 획득하는 API를 제공하지 않으므로, JPA 구현체가 제공하는 방법을 사용해야 한다.
Session session = entityManager.unwrap(Session.class);
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
// work...
}
});
- PA EntityManager에서 하이버네이트 Session을 구하고 session의 doWork() 메소드를 호출하면 된다.
- JDBC나 Mybatis를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다.
- 왜냐하면 JDBC를 사용하든 Mybatis를 사용하든 모두 JPA를 우회해서 데이터베이스를 접근하기 때문에 JPA가 전혀 인식하지 못하는
문제를 가지고 있다. -> 영속성 컨텍스트와 데이터베이스를불일치 상태
로 만들어데이터 무결성을 훼손
할 수 있다.- 해결 방법 : JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를
수동으로 플러시
해서 데이터베이스와 영속성 컨텍스트를 동기화하면 된다.
- 해결 방법 : JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를
JPQL
JPQL 사용방법
JPQL 특징
- JPQL은 객체지향 쿼리 언어이다. 테이블을 대상으로 쿼리하는 것이 아닌 엔티티 객체를 대상으로 쿼리한다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환한다.
기본 문법과 쿼리 API
- JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다.
- 엔티티를 저장할 때는 EntityManager.persist() 메소드를 사용하면 되므로 INSERT 문은 없다.
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
SELECT 문
- SELECT 문은 다음과 같다.
SELECT m FROM Member AS m where m.username = 'Hello'
- 대소문자 구분
- 엔티티와 속성은 대소문자를 구분한다. (Member, username) 반면에 SELECT, FROM, AS 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
- 엔티티 이름
- JPQL에서 사용한 Member는 클래스명이 아니라 엔티티이다. 엔티티명은
@Entity(name="이름")
로 지정할 수 있다. - 엔티티명을 지정하지 않으면 클래스명을 기본값으로 사용한다.
- JPQL에서 사용한 Member는 클래스명이 아니라 엔티티이다. 엔티티명은
- 별칭은 필수
- Member AS m을 보면 Member에 m이라는 별칭을 주었다.
- JPQL은 별칭을 필수로 사용해야 한다. 별칭없이 사용하면 잘못된 문법이라는 오류가 발생한다.
SELECT username FROM Member m // 잘못된 문법, username을 m.username 으로 고쳐야한다.
TypeQuery, Query
- 작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다.
쿼리 객체
- TypeQuery : 반환할 타입을
명확하게 지정할 수 있으면
사용 - Query : 반환 타입을
명확하게 지정할 수 없으면
사용
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class); // 명확하게 지정해서 사용 가능
List<Member> resultList = query.getResultList();
em.createQuery()의 두번째 파라미터에 반환할 타입을 지정
하면 TypeQuery를 반환
하고 지정하지 않으면 Query
를 반환한다.
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList(); // 명확하지 않다.
for(Object o : resultList) {
Object[] result = (Object[]) o; // 결과가 둘 이상이면 Object[] 반환
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
}
결과 조회
다음 메소들을 호출하면 실제 쿼리를 실행해서 데이터베이스를 조회
한다.
- query.getResultList() : 결과를 컬력션으로 반환한다. 만약 결과가 없으면 빈 컬렉션을 반환한다.
- query.getSingleResult() : 결과가 정확히 하나일 때 사용한다.
- 결과가 없으면
javax.persistence.NoResultException 예외
가 발생한다. - 결과가 1개보다 많으면
javax.persistence.NonUniqueResultException
예외가 발생한다.
- 결과가 없으면
10.2.2 파라미터 바인딩
JPQL은
- 위치 기준
- 이름 기준
파라미터 바인딩을 지원한다. 참고로 JDBC는 위치 기준 파라미터 바인딩만 지원한다.
이름 기준 파라미터
파라미터 이름으로 구분하는 방법이며 이름 앞에 :
를 사용한다.
String usernameParam = "User1";
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class);
query.setParameter("username", usernameParam);
List<Member> resultList = query.getResultList();
위치 기준 파라미터
?
다음에 위치 값을 주면 된다. 위치 값은 1부터 시작한다.
List<Member> members = em.createQuery("SELECT m FROM m WHERE m.username
=?1", Member.class)
.setParameter(1, usernameParam)
.getResultList();
위치 기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.
파라미터 바인딩 주의할 점
// 파라미터 바인딩 방식을 사용하지 않고 직접 SQL을 만들면 위험하다.
"select m from Member m where m.username = '" + usernameParam + "'"
- 파라미터 바인딩 방식을 사용하지 않고 직접 문자열을 더해 만들어 넣으면 악의적인 사용자에 의해 SQL 인젝션 공격을 당할 수 있다.
- 또한 성능 이슈도 있는데 파라미터 바인딩 방식을 사용하면 애플리케이션과 데이터베이스 모두 해당 쿼리의 파싱 결과를 재사용할 수 있어서 전체 성능이 향상된다.
- 추가적으로 문자열 + 연산을 사용하면 String.concat 메소드를 사용하는게 아니라 StringBuilder 클래스를 만든 후 다시 문자열을 돌려줘 성능이 떨어지고 메모리 효율성이 떨어진다.
- 따라서 파라미터 바인딩 방식은 선택이 아닌 필수이다.
프로젝션
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션
이라 하고 [SELECT {프로젝션 대상} FROM]
으로 대상을 선택한다.
- 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다. 스칼라 타입은 숫자, 문자 등 기본 데이터 타입을 뜻한다.
1. 엔티티 프로젝션
SELECT m FROM Member m // 회원
SELECT m.team FROM Member m // 팀
- 원하는 엔티티 객체를 프로젝션 대상으로 사용한 것이며 컬럼을 하나하나 나열해서 조회해야 하는 SQL과 차이가 있다.
이렇게 조회한 엔티티는 영속성 컨텍스트
에서 관리한다.
2. 임베디드 타입 프로젝션
엔티티 프로젝션과 거의 비슷하게 사용된다. 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.
String query = "SELECT a FROM Address a";
임베디드 타입인 Address를 조회의 시작점으로 사용해서 잘못된 쿼리이다.
String query = "SELECT o.address FROM Order o;
List<Address> address = em.createQuery(query, Address.class)
.getResultList();
Order 엔티티가 시작점이다. 엔티티를 통해서 임베디드 타입을 조회할 수 있다.
- 임베디드 타입은 엔티티 타입이 아닌 값 타입이다.
- 이렇게 직접 조회한
임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다
.
3. 스칼라 타입 프로젝션
- 숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다.
List<String> username = em.createQuery("SELECT username FROM Member m", String.class).getResultList();
- 여러 값 조회
- 엔티티를 대상으로 조회하면 편리하겠지만, 꼭 필요한 데이터들만 선택해서 조회해야 할 때도 있다.
- 프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신에 Query를 사용해야 한다.
Query query = em.createQuery("SELECT m.username, m.age FROM Member m"); List resultList = query.getResultList(); Iterator iterator = resultList.iterator(); while (iterator.hasNext()) { Object[] row = (Object[]) iterator.next(); String username = (String) row[0]; Integer age = (Integer) row[1]; }
- 제네릭에 Object[]를 사용하면 조금 더 간결하게 개발할 수 있다.
List<Object[]> resultList = em.createQuery("SELECT m.username, m.age FROM Member m") .getResultList(); for (Object[] row : resultList) { String username = (String) row[0]; Integer age = (Integer) row[1]; }
- 스칼라 타입뿐만 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있다.
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]; // 스칼라 }
이때도 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
DTO new 객체 생성
- NEW 명령어
- 직전에 username, age 두 필드를 프로젝션해서 지정할 수 없으므로 TypeQuery를 사용할 수 없어서 Object[]를 반환받았다.
- 실제 어플리케이션 개발시에는 Object[]를 직접 사용하지 않고 다음과 같이 DTO
객체를 생성하여 사용한다.
List<Object[]> resultList = em.createQuery("SELECT m.username, m.age FROM Member m").getResultList();
// 객체 변환 작업
List<UserDTO> userDTOs = new ArrayList<UserDTO>();
for(Object[] row : resultList) {
UserDTO userDTO = new UserDTO((String)row[0], (Integer)row[1]);
userDTOs.add(userDTO);
}
return userDTOs;
public class UserDTO {
private String username;
private int age;
public UserDTO(String username, int age) {
this.username = username;
this.age = age;
}
// ...
}
- 위의 작업을 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();
- SELECT 다음에 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있는데 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다.
- 그리고 NEW 명령어를 사용한 클래스로 TypeQuery 사용할 수 있어서 단순 변환 작업을 줄일 수 있다.
dto 사용시 주의 사항
- 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
- 순서와 타입이 일치하는 생성자가 필요하다.
페이징 API
데이터베이스마다 페이징 처리하는 SQL 문법이 다 다르다.
따라서 JPA는 페이징을 다음 두 API로 추상화 했다.
- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
- setMaxResults(int maxResult) : 조회할 테이터 수
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();
- 11번째부터 시작해서 총 20건의 데이터를 조회하는 예제이다.
- 따라서 11~30번 데이터를 조회한다.
- 데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언(Dialect) 덕분이다.
- JPQL이 방언에 따라 해당 데이터베이스 SQL 문으로 변환하기 때문이다.
- 만일, 페이징 SQL을 더 최적화하고 싶다면 네이티브 SQL을 직접 사용해야 한다.
10.2.5 집합과 정렬
집합 함수
집합은 집합함수와 함께 통계 정보를 구할 때 사용한다.
함수 | 설명 |
---|---|
COUNT | 결과 수를 구한다. 반환 타입 : Long |
MAX, MIN | 최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용 |
AVG | 평균값을 구한다. 숫자타입만 사용할 수 있다. 반환 타입 : Double |
SUM | 합을 구한다. 숫자타입만 사용할 수 있다. 반환 타입 : 정수합 Long, 소수합 Double, BigInteger합: BigInteger, BigDecimal합: BigDecimal |
집합 함수 사용 시 참고사항
- NULL 값은 무시하므로 통계에 잡히지 않는다(DISTINCT가 정의되어 있어도 무시)
- 만약 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL 값이 된다. 단 COUNT는 0이 된다.
- DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다.
예) SELECT COUNT(DISTINCT m.age) FROM Member m - DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.
GROUP BY, HAVING
- GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다.
- HAVING은 GROUP BY와 함께 사용하는데 GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링한다.
문법은 다음과 같다.
groupby_절 :: = GROUP BY {단일값 경로 | 별칭} +
having_절 :: = HAVING 조건식
select t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age),
MIN(m.age)
from Member m LEFT JOIN m.team t
GROUP BY t.name
HAVING AVG(m.age) >= 10
그룹별 통계 데이터 중에서 평균나이가 10살 이상인 그룹을 조회
이런 쿼리들은 보통 리포팅 쿼리나 통계 쿼리라 한다.
정렬(ORDER BY)
ORDER BY는 결과를 정렬할 때 사용한다.
문법은 다음과 같다.
orderby_절 :: = ORDER BY {상태필드 경로 | 결과 변수 {ASC | DESC}}+
select t.name, COUNT(m.age) as cnt
from Member m LEFT JOIN m.team t
GROUP BY t.name
ORDER BY cnt
- ASC : 오름차순(기본값)
- DESC : 내림차순
10.2.6 JPQL 조인
JPQL도 조인을 지원하는데 SQL 조인과 기능은 같고 문법만 약간 다르다.
내부 조인
내부 조인은 INNER JOIN을 사용한다. (INNER 생략 가능)
String teamName = "팀A";
String query = "SELECT m FROM Member m INNER JOIN m.team t "
+ "WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
.setParameter("teamName", teamName)
.getResultList();
SELECT m
FROM Member m INNER JOIN m.team t
WHERE t.name = :teamName
- JPQL 조인의 가장 큰 특징은 연관 필드를 사용하는 것인데 연관필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말한다.
- 여기서는 m.team 필드이다.
- FROM Member m : 회원 엔티티를 선택하고 별칭을 m 이라고 주었다.
- Member m JOIN m.team t : 회원이 가지고 있는 연관 필드로 팀과 조인한다
- 조인한 팀에는 t라는 별칭을 주었다.
- 만약 조인한 두 개의 엔티티를 조회하려면 다음과 같이 JPQL 작성하면 된다.
SELECT m, t
FROM Member m JOIN m.team t
List<Object[]> result = em.createQuery(query).getResultList();
for(Object[] row : result) {
Member member = (Member) row[0];
Team team = (Team) row[1];
}
- 서로 다른 타입의 두 엔티티를 조회했으므로
TypeQuery를 사용할 수 없다
.
외부 조인
외부 조인은 기능상 SQL의 외부 조인과 같다.
OUTER는 생략 가능
, 보통 LEFT JOIN으로 사용한다.
SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t
컬렉션 조인
일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.
- [회원 -> 팀]으로의 조인은 다대일 조인이면서 단일 값 연관 필드(m.team)를 사용
- [팀 -> 회원]은 반대로 일대다 조인이면서 컬렉션 값 연관 필드(m.members)를 사용
SELECT t, m FROM Team t LEFT JOIN t.members m
- 연관된 객체인 컬렉션을 조인했다.
- 여기서는 팀과 팀이 보유한 회원목록을 컬렉션 값 연관 필드로 외부 조인했다.
세타 조인
WHERE 절을 사용해서 세타 조인을 사용할 수 있다.
참고로 세타 조인은 내부 조인만 지원한다.
JOIN ON 절
- JPA 2.1 부터 조인할때 ON 절을 지원한다.
- ON절을 사용하면 조인 대상을 필터링하고 조인할 수 있다.
- 내부 조인의 ON 절은 WHERE 절을 사용할 때와 결과가 같으므로 보통 ON 절은 외부 조인에서만 사용한다.
// 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'
SQL 결과를 보면 and t.name='A'로 조인 시점에 조인 대상을 필터링한다.
페치 조인 (fetch join)
- 페치 조인은 SQL에서 이야기하는 조인의 종류는 아니고 JPQL에서 성능 최적화를 하기 위해 제공하는 가능이다.
- 이것은 연관된 엔티티나 컬렉션을
한번에 같이 조회하는 기능
인데 join fetch 명령어로 사용할 수 있다. - 페치 조인 문법은 다음과 같다.
페치 조인 :: = [ LEFT [OUTER] | INNER ] JOIN FETCH 조인 경로
- JOIN FETCH 문법이 핵심이다.
엔티티 페치 조인
select m
from Member m join fetch m.team
- join 다음에 fetch를 적었다. 이렇게 하면 연관된 엔티티나 컬렉션을 함께 조회하는데 여기서는 회원(m)과
팀(m.team)을 함께 조회한다. - 참고로 일반적인 JPQL 조인과는 다르게 m.team 다음에 별칭이 없는데
페치 조인은 별칭을 사용할 수 없다.
- 실행된 SQL은 다음과 같다.
SELECT
M.*, T.*
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID = T.ID
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());
}
- 지연로딩으로 설정해도 페치조인으로 조회하면 같이 조회된다.
- 즉 연관된 엔티티는 프록시가 아닌 실제 조회된 엔티티이다.
- 프록시가 아닌 실제 엔티티이므로 회원 엔티티가
영속성 컨텍스트에서 분리
되어준영속 상태가 되어도
연관된 팀을 조회할 수 있다.
페치 조인과 DISTINCT
- SQL의 DISTINCT는 중복된 결과를 제거하는 명령이다.
- JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것과 추가적으로
애플리케이션에서 한 번 더 중복을 제거한다.
select distinct
from Team t join fetch t.members
where t.name = '팀A'
- 직전에 컬렉션 페치 조인했던 쿼리에 DISTINCT 추가
- SQL의 DISTINCT는 중복된 데이터가 없기 때문에 효과가 없다.
- 애플리케이션에서 distinct을 사용해 엔티티 중복을 제거하라는 것이다. 따라서 팀이 하나만 조회가 된다.
페치 조인과 일반 조인의 차이
select t
from Team t join t.members m
where t.name = '팀A'
SELECT
T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = '팀A'
- 페치 조인을 사용하지 않고 조인만 사용하면 위와 같다.
- JPQL에서 팀과 회원 컬렉션을 조인했으므로 회원 컬렉션도 함께 조회할 것으로 기대하면 안된다.
JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
- 만약 회원 컬렉션을 지연 로딩으로 설정하면 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환한다.
- 즉시 로딩으로 설정하면 회원 컬럭션을 즉시 로딩하기 위해 쿼리를 한 번 더 실행한다.
반면에 페치 조인을 사용하면 연관된 엔티티도 함께 조회한다.
select t
from Team t join fetch t.members
where t.name = '팀A'
SELECT
T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = '팀A'
페치 조인의 특징과 한계
- 페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여
성능을 최적화할 수 있다.
- 엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치므로
글로벌 로딩 전략
이라 부른다. 페치 조인은 글로벌 로딩 전략보다 우선한다.
- 예를 들어 글로벌 로딩 전략을 지연 로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치 조인을 적용해서 함께 조회된다.
@OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
- 최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에서 항상 즉시 로딩이 일어난다.
- 일부는 빠를 수 있지만 전체로 보면 사용하지 않는 엔티티를 자주 로딩하므로 오히려 성능에 악영향을 미칠 수 있다.
- 글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다.
- 또한 페치 조인을 사용하면 연관된 엔티티를 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않는다.
- 따라서 준영속 상태에서도 객체 그래프를 탐색할 수 있다.
페치 조인은 다음과 같은 한계가 있다.
페치 조인 대상에는 별칭을 줄 수 없다.
- 별칭을 정의하는 내용이 없어 SELECT, WHERE 절, 서브 쿼리에 페치 조인 대상을 사용할 수 없다.
- JPA 표준에서는 지원하지 않지만 하이버네이트를 포함한 몇몇 구현체들은 별칭을 지원한다.
- 하지만 별칭을 잘못 사용하면 데이터 무결성이 깨질 수 있으므로 조심히 사용해야 한다.
- 특히 2차 캐시와 함께 저장되면 다른 곳에서 조회할 때도 연관된 데이터 수가 달라지는 문제가 발생할 수 있다. (16장에서 설명)
- 둘 이상의 컬렉션을 페치할 수 없다.
- 구현체에 따라 되기도 하는데 컬렉션 * 컬렉션의 카테시안 곱이 만들어지므로 주의해야 한다.
- 하이버네이트를 사용하면
javax.persistence.PersistenceException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
예외가 발생한다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
- 컬렉션(일대다)이 아닌 단일 값 연관 필드(일대일, 다대일)들은 페치 조인을 사용해도 페이징 API를 사용할 수 있다.
- 하이버네이트에서 컬렉션을 페치 조인하고 페이징 API를 사용하면 경고 로그를 남기면서 메모리에서 페이칭 처리를 한다.
- 데이터가 적으면 상관없지만 데이터가 많으면 성능 이슈와 메모리 초과 예외가 발생할 수 있어서 위험하다.
- 페치 조인은 SQL 한 번으로 연관된 여러 엔티티를 조회할 수 있어서 성능 최적화에 상당히 유용하다.
- 그리고 실무에서 자주 사용하게 된다. 하지만 모든 것을 페치 조인으로 해결할 수 없다.
- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이며 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 억지로 페치 조인을 하기보다는
DTO로 반환하는것이 더 효과적일 수 있다.
10.2.8 경로 표현식
경로 표현식은 쉽게 .(점)을 찍어 객체 그래프를 탐색하는 것이다.
select m.username
from Member m
join m.team t
join m.orders o
where t.name = '팀A'
여기서 m.username, m.team, m.orders. t.name이 모두 경로 표현식을 사용한 예이다.
경로 표현식 용어 정리
- 상태 필드 : 단순히 값을 저장하기 위한 필드(필드 or 프로퍼티)
- 연관 필드 : 객체 사이 연관관계를 맺기 위한 필드, 임베디드 타입 포함(필드 or 프로퍼티)
- 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티
- 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "name")
private String username; // 상태필드
private Integer age; // 상태 필드
@ManyToOne(...)
private Team team; // 연관 필드(단일 값 연관 필드)
@OneToMany(...)
private List<Order> orders; // 연관 필드(컬렉션 값 연관 필드)
}
- 상태 필드 : t.username, t.age
- 단일 값 연관 필드 : m.team
- 컬렉션 값 연관 필드 : m.orders
경로 표현식과 특징
JPQL에서 경로 표현식을 사용해서 경로 탐색을 하려면 다음 3가지 경로에 따라 어떤 특징이 있는지 이해해야 한다.
- 상태 필드 경로 : 경로 탐색의 끝이며 더는 탐색할 수 없다.
- 단일 값 연관 경로 : 묵시적으로 내부 조인이 일어나며 단일 값 연관 경로는 계속 탐색할 수 있다.
- 컬렉션 값 연관 경로 : 묵시적으로 내부 조인이 일어나며 더는 탐색할 수 없다. 단 FROM 절에서
조인을 통해 별칭을 얻으면 별칭으로 탐색이 가능하다.
상태 필드 경로 탐색
select m.username, m.age from Member m // JPQL
select m.name, m.age from Member m // SQL
단일 값 연관 경로 탐색
select o.member from Order o // JPQL
select m.*
from Orders o
inner join Member m on o.member_id = m.id
JPQL을 보면 o.member 연관 필드를 통해 주문에서 회원으로 경로 탐색을 했다.
단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이것을 묵시적 조인이라 한다.
참고로 묵시적 조인은 모두 내부 조인이다. 외부 조인은 명시적으로 JOIN 키워드를 사용해야 한다.
- 명시적 조인 : JOIN을 직접 적어주는 것
SELECT m FROM Member m JOIN m.team t
- 묵시적 조인 : 경로 표현식에 의해 묵시적으로 조인이 일어나는 것, 내부 조인
INNER JOIN
만 할 수 있다.SELECT m.team FROM Member m
컬렉션 값 연관 경로 탐색
JPQL을 다루면서 많이 하는 실수 중 하나는 컬렉션 값에서 경로 탐색을 시도하는 것이다.
select t.members from Team t // 성공
select t.members.username from Team t // 실패
t.members처럼 컬렉션까지는 경로 탐색이 가능하다. 하지만 t.members.username처럼 컬렉션에서
경로 탐색을 시작하는 것은 허락하지 않는다. 만약 컬렉션에서 경로 탐색을 하고 싶으면 조인해서 새로운 별칭을 얻어야한다.
select m.username from Team t join t.members m
join을 통해 members 컬렉션에 새로운 별칭을 얻었다. 별칭을 통해 경로 탐색이 가능하다.
경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인이다.
- 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야한다.
- 경로 탐색은 주로 SELECT, WHERE 절(다른 곳에서도 사용됨)에서 사용하지만 묵시적 조인으로 인해
SQL의 FROM 절에 영향을 준다.
조인이 성능상 차지하는 부분이 크다. 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵다는 단점이 있다.
따라서 성능에 이슈가 발생했을때 분석하기 쉽도록 묵시적 조인보다는 명시적 조인을 사용하는것이 좋다.
10.2.9 서브 쿼리
JPQL도 SQL처럼 서브쿼리를 지원한다. 몇가지 제약이 있는데 WHERE, HAVING 절에서만 사용할 수 있고
SELECT, FROM 절에서는 사용할 수 없다.
서브 쿼리 함수
- [NOT] EXISTS (subquery)
- {ALL | ANY | SOME} (subquery)
- [NOT] IN (subquery)
EXISTS
문법 : [NOT] EXISTS (subquery)
설명 : 서브쿼리에 결과가 존재하면 참이다. NOT은 반대
select m from Member m
where exists (select t from m.team t where t.name = '팀A'
- 팀A 소속인 회원 여부
{ALL | ANY | SOME}
문법 : {ALL | ANY | SOME} (subquery)
설명 : 비교 연산자와 같이 사용한다. {= | > | >= | < | <= | <>}
- ALL : 조건을 모두 만족하면 참이다.
- ANY 혹은 SOME : 둘은 같은 의미다. 조건을 하나라도 만족하면 참이다.
select o from Orders o
where o.orderAmount > ALL (select p.stockAmount from Product p)
- 전체 상품 각각의 재고보다 주문량이 많은 주문들
IN
문법 : [NOT] IN (subquery)
설명 : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참이다. 참고로 IN은 서브쿼리가 아닌 곳에서도 사용한다.
select t from Team t
where t IN (select t2 From Team t2 JOIN t2.members m2 where m2.age >= 20)
- 20세 이상을 보유한 팀
10.2.10 조건식
종류 | 설명 | 예제 |
---|---|---|
문자 | 작은 따옴표 사이에 표현 작은 따옴표를 표현하고 싶으면 작은 따옴표 연속 두개 사용 |
'HELLO' 'She''s' |
숫자 | L(Long 타입 지정) D(Double 타입 지정) F(Float 타입 지정) |
10L 10D 10F |
날짜 | DATE{d 'yyyy-mm-dd'} TIME {t 'hh-mm-ss'} DATETIME(ts 'yyyy-mm-dd hh:mm:ss.f'} |
{d '2012-03-24'} {t '10-11-11'} {ts '2012-03-24 10-11-11.123'} m.createDate = {d '2012-03-24'} |
Boolean | TRUE, FALSE | |
Enum | 패키지명을 포함한 전체 이름을 사용해야 한다. | jpabook.MemberType.Admin |
엔티티 타입 | 엔티티의 타입을 표현한다. 주로 상속과 관련해서 사용한다. | TYPE(m) = Member |
연산자 우선 순위
연산자 우선 순위는 다음과 같다.
- 경로 탐색 연산(.)
- 수학 연산: +, -(단항 연산자), *, /, +, -
- 비교 연산 : =, >, >=, <, <=, <>(다름), [NOT] BETWEEN, [NOT] LIKE, [NOT] IN, IS [NOT] NULL,
IS [NOT] EMPTY, [NOT] MEMBER [OF], [NOT] EXISTS - 논리 연산 : NOT, AND, OR
논리 연산과 비교식
- 논리 연산
- AND : 둘 다 만족하면 참
- OR : 둘 중 하나만 만족해도 참
- NOT : 조건식의 결과 반대
- 비교식
비교식은 다음과 같다.
= | > | >= | < | <= | <>
Between, IN, Like, NULL 비교
- Between 식
문법 : X [NOT] BETWEEN A AND B
설명 : X는 A ~ B 사이의 값이면 참(A, B 값 포함)나이가 10 ~ 20인 회원을 찾을때 select m from Member m where m.age between 10 and 20
- IN 식
문법 : X [NOT] IN (예제)
설명 : X와 같은 값이 예제에 하나라도 있으면 참이다. IN 식의 예제에는 서브쿼리를 사용할 수 있다.이름이 회원1이나 회원2인 회원을 찾을때 select m from Member m where m.username in ('회원1', '회원2')
- Like 식
문법 : 문자표현식 [NOT] LIKE 패턴값 [ESCAPE 이스케이프 문자]
설명 : 문자표현식과 패턴값을 비교한다.- %(퍼센트) : 아무 값들이 입력되어도 된다.(값이 없어도 됨)
- _(언더라인) : 한 글자는 아무 값이 입력되어도 되지만 값이 있어야 한다.
- NULL 비교식
문법 : {단일값 경로 | 입력 파라미터 | IS [NOT] NULL }
설명 : NULL 인지 비교한다. NULL은 =으로 비교하면 안되고 꼭 IS NULL을 사용해야 한다. where m.username is null where null = null // 거짓 where 1=1 // 참
컬렉션 식
컬렉션 식은 컬렉션에만 사용하는 특별한 기능이다. 참고로 컬렉션은 컬렉션 식 이외에 다른 식은 사용할 수 없다.
- 빈 컨렉션 비교 식
문법 : {컬렉션 값 연관 경로} IS [NOT] EMPTY
설명 : 컬렉션에 값이 비어있으면 참빈 컬렉션을 비교하는 예제이며 컬렉션은 컬렉션 식만 사용할 수 있다는 점에 주의! // JPQL : 주문이 하나라도 있는 회원 조회 select m from Member m where m.orders is not empty // 실행된 SQL select m.* from Member m where exists { select o.id from Orders o where m.id = o.member_id }
- 컬렉션의 멤버 식
문법 : {엔티티나 값} [NOT] MEMBER [OF] {컬렉션 값 연관 경로}
설명 : 엔티티나 값이 컬렉션에 포함되어 있으면 참 select t from Team t where :memberParam member of t.members
스칼라 식
스칼라는 숫자, 문자, 날짜, case, 엔티티 타입(엔티티의 타입 정보) 같은 가장 기본적인 타입들을 말한다.
- 수학식
- +,- : 단항 연산자
- *,/,+,- : 사칙 연산
- 문자함수
함수 | 설명 | 예제 |
---|---|---|
CONCAT(문자1, 문자2, ...) | 문자를 합한다. | CONCAT('A','B') = AB |
SUBSTRING(문자, 위치, [길이]) | 위치부터 시작해 길이만큼 문자를 구한다. 길이 값이 없으면 나머지 전체 길이를 뜻한다. |
SUBSTRING('ABCDEF', 2, 3) = BCD |
TRIM([[LEADING /TRAILING / BOTH] [트림 문자] FROM]문자) | LEADING: 왼쪽만 TRAILING: 오른쪽만 BOTH: 양쪽 다 트림 문자를 제거한다. 기본값은 공백이다. |
TRIM(' ABC ') = 'ABC' |
LOWER(문자) | 소문자로 변경 | LOWER('ABC') = 'abc' |
UPPER(문자) | 대문자로 변경 | UPPER('abc') = 'ABC' |
LENGTH(문자) | 문자 길이 | LENGTH('ABC') = 3 |
LOCATE(찾을 문자, 원본 문자, [검색시작위치]) | 검색위치부터 문자를 검색한다. 1부터 시작, 못 찾으면 0 반환 | LOCATE('DE', 'ABCDEFG') = 4 |
- 수학 함수
함수 | 설명 | 예제 |
---|---|---|
ABS(수학식) | 절대값을 구한다. | ABS(-10) = 10 |
SQRT(수학식) | 제곱근을 구한다. | SQRT(4) = 2.0 |
MOD(수학식, 나눌 수) | 나머지를 구한다. | MOD(4,3) = 1 |
SIZE(컬렉션 값 연관 경로식) | 콜렉션의 크기를 구한다. | SIZE(t.members) |
INDEX(별칭) | LIST 타입 컬렉션의 위치값을 구함. 단 컬렉션이 @OrderColumn을 사용하는 LIST 타입일 때만 사용할 수 있다. |
t.members m where INDEX(m) > 3 |
- 날짜 함수
날짜함수는 데이터베이스의 현재 시간을 조회- CURRENT_DATE : 현재 날짜
- CURRENT_TIME : 현재 시간
- CURRENT_TIMESTAMP : 현재 날짜 시간
select CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP from Team t
// 결과: 2013-08-19, 23:38:17, 2013-08-19 23:38:17.736
- 하이버네이트는 날짜 타입에서 년, 월, 일, 시간, 분, 초 값을 구하는 기능을 지원한다.
- YEAR, MONTH, DAY, HOUR, MINUTE, SECOND
select year(CURRENT_TIMESTAMP), month(CURRENT_TIMESTAMP), day(CURRENT_TIMESTAMP) from Member
- 데이터베이스들은 각자의 방식으로 더 많은 날짜 함수를 지원한다.
- 그리고 각각의 날짜 함수는 하이버네이트가제공하는 데이터베이스 방언에 등록되어 있다.
CASE 식
특정 조건에 따라 분기할 때 CASE 식을 사용한다.
- 기본 CASE
- 심플 CASE
- COALESCE
- NULLIF
- 기본 CASE
문법 : CASE {WHEN <조건식> THEN <스칼라식>} + ELSE <스칼라식> END 예 select case when m.age <= 10 then '학생 요금' when m.age >= 60 then '경로 요금' else '일반 요금' end from Member m
- 심플 CASE
심플 CASE는 조건식을 사용할 수 없지만, 문법이 단순하다. 문법 : CASE <조건대상> {WHEN <스칼라식1> THEN <스칼라식2>} + ELSE <스칼라식> END 예 select case t.name when '팀A' then '인센티브100%' when '팀B' then '인센티브120%' else '인센티브105%' end from Team t
- COALESCE
문법 : COALESCE(<스칼라식> {,<스칼라식>}+)
설명 : 스칼라식을 차례대로 조회해서 null이 아니면 반환한다. select coalesce(m.username, '이름 없는 회원') from Member m 예 m.username이 null이면 '이름 없는 회원'을 반환
- NULLIF
문법 : NULLIF(<스칼라식>, <스칼라식>)
설명 : 두 값이 같으면 null을 반환하고 다르면 첫 번째 값을 반환한다. 집합 함수는 null을 포함하지
않으므로 보통 집합 함수와 함께 사용한다. select NULLIF(m.username, '관리자') from Member m 예 사용자 이름이 '관리자'면 null을 반환하고 나머지는 본인의 이름을 반환
10.2.11 다형성 쿼리
JPQL로 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회한다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
//...
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
//...
private String author;
}
// Album, Movie 생략
- 다음과 같이 조회하면 Item의 자식도 함께 조회
List resultList = em.createQuery("select i from Item i").getResultList();
- 단일 테이블 전략(InheritanceType.SINGLE_TABLE)을 사용할 때 실행되는 SQL은 다음과 같다.
SELECT * FROM ITEM // SQL
- 조인 전략(InheritanceType.JOINED)을 사용할 때 실행되는 SQL은 다음과 같다.
// SQL
SELECT
i.ITEM_ID, i.DTYPE, i.name, i.price, i.stockQuantity,
b.author, b.isbn,
a.artist, a.etc,
m.actor, m.director
FROM
Item i
left outer join
Book b on i.ITEM_ID = b.ITEM_ID
left outer join
Album a on i.ITEM_ID = a.ITEM_ID
left outer join
Movie m on i.ITEM_ID = m.ITEM_ID
TYPE
- 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)
- TREAT는 JPA 2.1에 추가된 기능인데 자바의 타입 캐스팅과 비슷하다. 상속 구조에서 부모 타입을 특정 타입으로 다룰 때 사용한다.
- JPA 표준은 FROM, WHERE 절에서 사용할 수 있지만, 하이버네이트는 SELECT 절에서도 TREAT를 사용할 수 있다.
// 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'
- JPQL을 보면 treat를 사용해서 부모 타입인 Item을 자식 타입인 Book으로 다룬다. 따라서 author 필드에 접근할 수 있다.
### 10.2.12 사용자 정의 함수 호출(JPA 2.1)
문법:
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));
}
}
- hibernate.dialect에 해당 방언을 등록해야 한다.
<property name="hibernate.dialect" value="hello.MyH2Dialect" />
- 하이버네이트 구현체를 사용하면 다음과 같이 축약해서 사용할 수 있다.
select group_concat (i.name) from Item i
10.2.13 기타 정리
- enum은 = 비교 연산만 지원
- 임베디드 타입은 비교를 지원하지 않는다.
EMPTY STRING
JPA 표준은 '길이 0인 Empty String으로 정했지만 데이터베이스에 따라'를 NULL로 사용하는 데이터베이스도 있으므로 확인하고 사용해야 한다.
NULL 정의
- 조건을 만족하는 데이터가 하나도 없으면 NULL이다.
- NULL은 알 수 없는 값아다. NULL과의 모든 수학적 계산 결과는 NULL이 된다.
- NULL == NULL은 알 수 없는값이다.
- NULL is NULL 은 참이다.
JPA 표준 명세는 Null(u) 값과 TRUE(T), FALSE(F)의 논리 계산을 다음과 같이 정의했다.
AND
AND | T | F | U |
---|---|---|---|
T | T | F | U |
F | F | F | F |
U | U | F | U |
OR | T | F | U |
---|---|---|---|
T | T | T | T |
F | T | F | U |
U | T | U | U |
NOT | |
---|---|
T | F |
F | T |
U | U |
10.2.14 엔티티 직접 사용
기본 키 값
- 객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본 키 값으로 식별한다.
따라서 JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
select count(m.id) from Member m // 엔티티의 아이디를 사용
select count(m) from Member m // 엔티티를 직접 사용
- 두 번째의 count(m)을 보면 엔티티의 별칭을 직접 넘겨주었다.
- 이렇게 엔티티를 직접 사용하면 JPQL이 SQL로 변환할 때 해당 엔티티의 기본 키를 사용한다.
// SQL 문
select count(m.id) as cnt
from Member m
JPQL의 count(m)이 SQL에서 count(m.id)로 변환된 것을 확인할 수 있다.
String qlString = "select m from Member m where m = :member";
List resultList = em.createQuery(qlString)
.getParameter("member", member)
.getResultList();
// 실행 SQL
select m.*
from Member m
where m.id = ?
외래 키 값
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();
- 기본 키 값이 1L인 팀 엔티티를 파라미터로 사용하고 있다.
- m.team은 현재 team_id라는
외래 키와 매핑되어 있다. 따라서 다음과 같은 SQL이 실행된다.
select m.*
from Member m
where m.team_id=?(팀 파라미터의 ID 값)
10.2.15 Named 쿼리: 정적 쿼리
JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있다.
- 동적 쿼리 :
- em.createQuery("select ..")처럼 JPQL을 문자로 완성해서 직접 넘기는 것을 동적 쿼리라 한다.
- 런타임에 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다.
- 정적 쿼리 :
- 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을 Named 쿼리라 한다.
- Named 쿼리는 한 번 정의하면 변경할 수 없는 정적인 쿼리이다.
Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해 둔다.
따라서 오류를 빨리 확인할 수 있고, 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다.
그리고 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스의 조회 성능 최적화에는 도움이 된다.
Named 쿼리는 @NamedQuery 어노테이션을 사용해서 자바 코드에 작성하거나 XML 문서에 작성할 수 있다.
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", "회원1")
.getResultList();
- Named 쿼리는 영속성 단위로 관리되므로 @NamedQuery 안에 name 선언을 통하여 충돌을 방지해 줄 수 있다.
- 하나의 엔티티에 2개 이상의 Named 쿼리를 정의하려면 @NamedQuries 어노테이션을 사용하면 된다.
@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(); // Named 쿼리 이름 (필수)
String query(); // JPQL 정의 (필수)
LockModelType lockMode() default NONE; // 쿼리 실행 시 락모드를 설정할 수 있다.
QueryHint[] hints() default {}; // JPA 구현체에 쿼리 힌트를 줄 수 있다.
}
- lockMode : 쿼리 실행 시 락을 건다.
- hints : 여기서 힌트는 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트다. 예를 들어 2차 캐시를 다룰 때 사용
Named 쿼리를 XML에 정의
- 어노테이션 방법으로 사용할 때는 직관적이고 편리하지만 Named 쿼리를 작성할 때는 XML을 사용하는 것이 더 편리하다.
- 코드가 더러워질 염려가 있다.
- 자바 언어로 멀티라인 문자를 다루는 것은 상당히 불편한데 이를 해결하려면 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>
- ormMember.xml을 인식하도록 META-INF/persistence.xml에 다음 코드를 추가한다.
<persistence-unit name="jpabook" >
<mapping-file>META-INF/ormMember.xml</mapping-file>
</persistence-unit>
환경에 따른 설정
- 만약 XML과 어노테이션에 같은 설정이 있으면 XML이 우선권을 가진다.
- 따라서 운영 환경에 따라 다른 쿼리를 실행해야 한다면 각 환경에 맞춘 XML을 준비해 두고 XML만 변경해서 배포하면 된다.
10.3 Criteria
- Criteria 쿼리는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API이다.
- Criteria를 사용하면 문자가 코드로 JPQL을 작성하므로 문법 오류를 컴파일 단계에서 잡을 수 있고
- 문자 기반의 JPQL보다 동적 쿼리를 안전하게 생성할 수 있다는 장점이 있다.
- 하지만 실제 Criteria를 사용해서 개발하면 코드가 복잡하고 장황해서 직관적으로 이해하기 어렵다는 단점이 있다.
10.3.1 Criteria 기초
Criteria API는 javax.persistence.criteria 패키지에 있다.
// JPQL: select m from Member m
CriteriaBuilder cb = em.getCriteriaBuilder(); // Criteria 쿼리 빌더 (1)
// Criteria 생성, 반환 타입 지정 (2)
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class); // FROM 절 (3)
cq.select(m); // SELECT 절 (4)
TypedQuery<Member> query = em.createQuery(cq);
List<Member> members = query.getResultList();
- Criteria 쿼리를 생성하려면 먼저 Criteria 빌더를 얻어야 한다. 빌더는 EntityManager나 EntityManagerFactory에서 얻을 수 있다.
- Criteria 쿼리 빌더에서 Criteria 쿼리를 생성한다. 이때 반환 타입을 지정할 수 있다.
- FROM 절을 생성한다. 반환된 값 m은 Criteria에서 사용하는 특별한 별칭이다. m을 조회의 시작점이라는 의미로 쿼리 루트(Root)라 한다.
- SELECT 절을 생성한다.
// JPQL
// select m from Member m
// where m.username='회원1'
// order by m.age desc
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class); // FROM 절
// 검색 조건 정의 (1)
Predicate usernameEqual = cb.equal(m.get("username"), "회원1");
// 정렬 조건 정의 (2)
javax.persistence.criteria.Order ageDesc = cb.desc(m.get("age"));
// 쿼리 생성 (3)
cq.select(m)
.where(usernameEqual) // WHERE 절 생성
.orderBy(ageDesc); // ORDER BY 절 생성
List<Member> resultList = em.createQuery(cq).getResultList();
- 검색 조건을 정의한 부분을 보면 m.get("username")으로 되어 있는데 m은 회원 엔티티의 별칭이다.
이것은 JPQL에서 m.username과 같은 표현이다. 그리고 cb.equal(A,B)는 이름 그대로 A = B 라는 뜻이다.
따라서 JPQL에서 m.username = '회원1'과 같은 표현이다. - 정렬 조건을 정의하는 코드인 cb.desc(m.get("age"))는 JPQL의 m.age desc와 같은 표현이다.
- 만들어둔 조건을 where, orderBy에 넣어서 원하는 쿼리를 생성한다.
10.3.2 Criteria 쿼리 생성
Criteria를 생성하려면 CriteriaBuilder.createQuery() 메소드로 Criteria 쿼리를 생성하면 된다.
public interface CriteriaBuilder {
CriteriaQuery<Object> createQuery(); // 조회값 반환 타입 : Object
// 조회값 반환 타입: 엔티티, 임베디드 타입, 기타
<T> CriteriaQuery<T> createQuery(Class<T> resultClass);
CriteriaQuery<Tuple> createTupleQuery(); // 조회값 반환 타입 : Tuple
}
// 반환 타입 지정
CriteriaBuilder cb = em.getCriteriaBuilder();
// Member를 반환 타입으로 지정
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
// 위에서 Member를 타입으로 지정했으므로 지정하지 않아도 Member 타입을 반환
List<Member> resultList = em.createQuery(cq).getResultList();
//------------------------------------
// Object로 조회
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object> cq = cb.createQuery(); // 조회값 반환 타입: Obejct
List<Object> resultList = em.createQuery(cq).getResultList();
//------------------------------------
// Object[]로 조회
CriteriaBuilder cb = em.getCriteriaBuilder();
// 조회값 반환 타입: Object[]
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
List<Object[]> resultList = em.createQuery(cq).getResultList();
//------------------------------------
// 튜플로 조회
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createTupleQuery(); // 조회값 반환 타입: Tuple
TypedQuery<Tuple> query = em.createQuery(cq);
10.3.3 조회
public interface CriteriaQuery<T> extends AbstractQuery<T> {
// 한 건 지정
CriteriaQuery<T> select(Selection<? extends T> selection);
// 여러 건 지정
CriteriaQuery<T> multiselect(Selection<?>... selections);
// 여러 건 지정
CriteriaQuery<T> multiselect(List<Selection<?>> selectionList);
...
}
- select에 조회 대상을 하나만 지정하려면 다음처럼 작성
cq.select(m) // JPQL: select m
- 조회 대상을 여려 건을 지정하려면 multiselect를 사용하면 된다.
// JPQL : select m.username, m.age
cq.multiselect(m.get("username"), m.get("age"));
- 여러 건 지정은 다음처럼 cb.array를 사용해도 된다.
CriteriaBuilder db = em.getCriteriaBuilder();
// JPQL : select m.username, m.age
cq.select(cb.array(m.get("username"), m.get("age")));
DISTINCT
- distinct는 select, multiselect 다음에 distance(true)를 사용하면 된다.
// JPQL : select distinct m.username, m.age from Member m
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<Member> m = cq.from(Member.class);
cq.multiselect(m.get("username"), m.get("age")).distinct(true);
// cq.select(cb.array(m.get("username"), m.get("age"))).distinct(true); // 위 코드와 같다.
TypedQuery<Object[]> query = em.createQuery(cq);
List<Object[]> resultList = query.getResultList();
MEW.construct()
- JPQL에서 select new 생성자() 구문을 Criteria에서는 cb.construct(클래스 타입, ...) 로 사용한다.
// JPQL : select new jpabook.domain.MemberDTO(m.username, m.age) from Member m
CriteriaQuery<MemberDTO> cq = cb.createQuery(MemberDTO.class);
Root<Member> m = cq.from(Member.class);
cq.select(cb.construct(MemberDTO.class, m.get("username"), m.get("age")));
TypedQuery<MemberDTO> query = em.createQuery(cq);
List<MemberDTO> resultList = query.getResultList();
- JPQL에서는 풀 패키지명을 사용했다면 Criteria에서는 MemberDTO.class 처럼 간략하게 사용이 가능하다.
튜플
- Criteria는 Map과 비슷한 튜플이라는 특별한 객체를 제공한다.
// JPQL : select m.username, m.age from Member m
CriteriaBuilder db = em.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
// CriteriaQuery<Tuple> cq = cb.createQuery(Tuple.class); // 위와 같다.
Root<Member> m = cq.from(Member.class);
cq.multiselect(
m.get("username").alias("username"), // 튜플에서 사용할 튜플 별칭 (1)
m.get("age").alias("age")
);
TypedQuery<Tuple> query = em.createQuery(cq);
List<Tuple> resultList = query.getResultList();
for(Tuple tuple : resultList) {
// 튜플 별칭으로 조회 (2)
String username = tuple.get("username", String.class);
Integer age = tuple.get("age", Integer.class);
}
- 튜플은 튜플의 검색 키로 사용할 튜플 전용 별칭을 필수로 할당해야 하며, 별칭은 alias() 메소드를 사용해서 지정한다.
- 선언해둔 튜플 별칭으로 데이터를 조회할 수 있다.
튜플을 사용하려면 cb.createTupleQuery() 또는 cb.createQuery(Tuple.class)로 Criteria를 생성한다.
10.3.4 집합
GROUP BY
/*
JPQL :
select m.team.name, max(m.age), min(m.age)
from Member m
group by m.team.name
*/
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<Member> m = cq.from(Member.class);
Expression maxAge = cb.max(m.<Integer>get("age"));
Expression minAge = cb.min(m.<Integer>get("age"));
cq.multiselect(m.get("team").get("name"), maxAge, minAge);
cq.groupBy(m.get("team").get("name")); // GROUP BY
TypedQuery<Object[]> query = em.createQuery(cq);
List<Object[]> resultList = query.getResultList();
- cq.groupBy(m.get("team").get("name"))은 JPQL에서 group by m.team.name과 같다.
HAVING
팀에 가장 나이 어린 사람이 10살을 초과하는 팀을 조회하는 조건을 나타내면 다음과 같다.
cq.multiselect(m.get("team").get("name"), maxAge, minAge)
.groupBy(m.get("team").get("name"))
.having(cb.get(minAge, 10)); // HAVING
having(cb.get(minAge, 10)) 은 JPQL에서 having min(m.age) > 10과 같다.
10.3.5 정렬
정렬 조건도 Criteria 빌더를 통해서 생성
cb.desc(...) 또는 cb.asc(...)로 생성할 수 있다.
cq.select(m)
.where(ageGt)
.orderBy(cb.desc(m.get("age"))); // JPQL: order by m.age desc
10.3.6 조인
조인은 join() 메소드와 JoinType 클래스를 사용한다.
public enum JoinType {
INNER, // 내부 조인
LEFT, // 왼쪽 외부 조인
RIGHT // 오른쪽 외부 조인, JPA 구현체나 데이터베이스에 따라 지원하지 않을 수 있다.
}
/* JPQL
select m, t from Member m
inner join m.team t
where t.name = '팀A'
*/
Root<Member> m = cq.from(Member.class);
Join<Member, Team> t = m.join("team", JoinType.INNER); // 내부조인
cq.multiselect(m, t)
.where(cb.equal(t.get("name"), "팀A"));
- 쿼리 루트에서 바로 m.join("team") 메소드를 사용해서 회원과 팀을 조인했다.
- 그리고 조인한 team에 t라는 별칭을 주었다.
- 여기서는 JoinType.INNER를 설정해서 내부 조인을 사용했다.
- 참고로 내부 조인은 생략이 가능하고 외부 조인은 JoinType.LEFT로 설정하면 된다.
m.join("team") // 내부 조인
m.join("team", JoinType.INNER) // 내부 조인
m.join("team", JoinType.LEFT) // 외부 조인
- FETCH JOIN은 다음과 같이 사용
Root<Member> m = cq.from(Member.class);
m.fetch("team", JoinType.LEFT);
cq.select(m);
패치 조인은 fetch(조인대상, JoinType)을 사용한다.
10.3.7 서브 쿼리
- 간단한 서브 쿼리1 서브 쿼리 생성 부분을 보면 서브 쿼리는 mainQuery.subquery(...)로 생성한다.
2 메인 쿼리 생성 부분을 보면 where(... subQuery)에서 생성한 서브 쿼리를 사용한다. /* 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); // 서브 쿼리 생성 (1) Subquery<Dobule> subQuery = mainQuery.subquery(Double.class); Root<Member> m2 = subQuery.from(Member.class); subQuery.select(cb.avg(m2.<Integer>get("age"))); // 메인 쿼리 생성 (2) Root<Member> m = mainQuery.from(Member.class); mainQuery.select(m) .where(cb.ge(m.<Integer>get("get"), subQuery));
- 상호 관련 서브 쿼리
- 서브 쿼리에서 메인 쿼리의 정보를 사용하려면 메인 쿼리에서 사용한 별칭을 얻어야 한다.
/* JPQL : select m from Member m where exists (select t from m.team t where t.name = '팀A') */ CriteriaBuilder db = em.getCriteriaBuilder(); CriteriaQuery<Member> mainQuery = cb.createQuery(Member.class); // 서브 쿼리에서 사용하는 메인 쿼리의 m 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();
- 여기서 핵심은 subQuery.correlate(m)이다. correlate(...) 메소드를 사용하면 메인 쿼리의 별칭을 서브 쿼리에서 사용할 수 있다.
10.3.8 IN 식
/*
JPQL :
select m from Member m
where m.username in ("회원1", "회원2")
*/
...
cq.select(m)
.where(cb.in(m.get("username"))
.value("회원1")
.value("회원2"));
10.3.9 CASE 식
CASE 식에는 selectCase() 메소드와 when(), otherwise() 메소드들 사용한다.
/*
JPQL :
select m.username,
case when m.age >= 60 then 600
when m.age <= 15 then 500
else 1000
end
from Member m
*/
Root<Member> m = cq.from(Member.class);
cq.multiselect(
m.get("username"),
cb.selectCase()
.when(cb.ge(m.<Integer>get("age"), 60), 600)
.when(cb.ge(m.<Integer>get("age"), 15), 500)
.otherwise(1000)
);
10.3.10 파라미터 정의
JPQL에서 :PARAM1처럼 파라미터를 정의했듯이 Criteria도 파라미터를 정의할 수 있다.
/*
JPQL :
select m from Member m
where m.username = :usernameParam
*/
// 정의 (1)
cq.select(m)
.where(cb.equal(m.get("username"), cb.parameter(String.class, "usernameParam")));
List<Member> resultList = em.createQuery(cq)
.setParameter("usernameParam", "회원1") // 바인딩 (2)
.getResultList();
- cb.parameter(타입, 파라미터 이름) 메소드를 사용해서 파라미터를 정의했다.
- setParameter("usernameParam", "회원1")을 사용해서 해당 파라미터에 사용할 값을 바인딩했다.
10.3.11 네이티브 함수 호출
네이티브 SQL 함수를 호출하려면 cb.function(...) 메소드를 사용하면 된다.
Expression<Long> function = cb.function("SUM", Long.class, m.get("age"));
cq.select(function);
10.3.12 동적 쿼리
// 검색조건
Integer age = 10;
String username = null;
String teamName = "팀A";
// JPQL 동적 쿼리 생성
StringBuilder jpql = new StringBuilder("select m from Member m join m.team t ");
List<String> criteria = new ArrayList<String>();
if (age != null) criteria.add(" m.age = :age ");
if (username != null) criteria.add(" m.username = :username ");
if (teamName != null) criteria.add(" t.name = :teamName ");
if (criteria.size() > 0) jpql.append(" where ");
for (int i = 0; i <criteria.size(); i++) {
if (i > 0) jpql.append(" and ");
jpql.append(criteria.get(i));
}
TypedQuery<Member> query = em.createQuery(jpql.toString(), Member.class);
if (age != null) query.setParameter("age", age);
if (username != null) query.setParameter("username", username);
if (teamName != null) query.setParameter("teamName", teamName);
List<Member> resultList = query.getResultList();
// ------------------------------------------------------------
// Criteria 동적 쿼리 생성
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class);
Join<Member, Team> t = m.join("team");
List<Predicate> criteria = new ArrayList<Predicate>();
if (age != null) criteria.add(cb.equal(m.<Integer>get("age"), cb.parameter(Integer.class, "age")));
if (username != null) criteria.add(cb.equal(m.get("username"), db.parameter(String.class, "username")));
if (teamName != null) criteria.add(cb.equal(t.get("name"), cb.parameter(String.class, "teamName")));
cq.where(cb.and(criteria.toArray(new Predicate[0])));
TypedQuery<Member> query = em.createQuery(cq);
if (age != null) query.setParameter("age", age);
if (username != null) query.setParameter("username", username);
if (teamName != null) query.setParameter("teamName", teamName);
List<Member> resultList = query.getResultList();
- JPQL 로 동적쿼리를 작성하면 문자 더하기로 인해 여러 번 버그를 발생할 확률이 높다.
- 문자 사이에 공백을 입력하지 않아서
age=:ageandusername=:username
처럼 되기도 하고 where 와 and 위치를 구성하는 것도 신경써야 한다. - 반면 Criteria 로 동적 쿼리를 구성하면 최소한의 공백이나 where, and 위치로 인해 에러가 발생하지 않는다.
10.3.13 함수 정리
Criteria는 JPQL 빌더 역할을 하므로 JPQL 함수를 코드로 지원한다.
- Expression 메소드
함수명 | JPQL |
---|---|
isNull() | IS NULL |
isNotNull() | IS NOT NULL |
in() | IN |
- 조건 함수
함수명 | JPQL |
---|---|
and() | and |
or() | or |
not() | not |
equal(), notEqual() | =, <> |
lt(), lessThan() | < |
le(), LessThanOrEqualTo() | <= |
gt(), greaterThan() | > |
ge(), greaterThanOrEqualTo() | >= |
between() | between |
like(), notLike() | like, not like |
함수명 | JPQL |
---|---|
isTrue(), isFalse | is true, is false |
in(), not(in()) | in, not(in()) |
exists(), not(exists()) | exists, not exists |
isNull(), isNotNull() | is null, is not null |
isEmpty(), isNotEmpty() | is empty, is not empty |
isMember(), isNotMember() | member of, not member of |
- 스칼라와 기타 함수
함수명 | JPQL | 함수명 | JPQL |
---|---|---|---|
sum() | + | length() | length |
neg(), diff() | - | locate() | locate |
prod() | * | concat() | concat |
quot() | / | upper() | upper |
all() | all | lower() | lower |
any() | any | substring() | substring |
some() | some | trim() | trim |
abs() | abs | currentDate() | current_date |
sqrt() | sqrt | currentTime() | current_time |
mod() | mod | currentTimestamp() | current_timestamp |
size() | size |
- 집합 함수
함수명 | JPQL |
---|---|
avg() | avg |
max(), greatest() | max |
min(), least() | min |
sum(), sumAsLong(), sumAsDouble() | sum |
count() | count |
countDistinct() | count distinct |
- 분기 함수
함수명 | JPQL |
---|---|
nullif() | nullif |
coalesce() | coalesce |
selectCase() | case |
10.3.14 메타 모델 API
- Criteria는 코드 기반이므로 컴파일 시점에 오류를 발견할 수 있다. 하지만
m.get("age")
에서 age는 문자이다. age
대신에 실수로ageaaa
로 오타를 했을 때 컴파일 시점에 에러를 발견하지 못한다.- 따라서 완전한 코드 기반이라 할 수 없다. 이런 부분까지 코드로 작성하려면 메타 모델 API를 사용하면 된다.
// 메타 모델 API 사용전
cq.select(m)
.where(cb.gt(m.<Integer>get("username"), 20))
.orderBy(cb.desc(m.get("age")));
// 메타 모델 API 적용 후
cq.select(m)
.where(cb.gt(m.get(Member_.age), 20))
.orderBy(cb.desc(m.get(Member_.age)));
- 코드 기반으로 바꾸려면 Member_ 클래스가 필요한데 이것이 바로 메타 모델 클래스이다.
@Generated(value="org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Member.class)
public abstract class Member_ {
public static volatile SingularAttribute<Member, Integer> age;
...
}
- 이런 클래스를 표준 메타 모델 클래스라 부르며 줄여서 메타 모델이라 한다.
- 메타 모델 클래스는 Member 엔티티 기반으로 만들어야 하며 개발자가 직접 생성하지 않고 코드 자동 생성기가
- 엔티티 클래스 기반으로 메타 모델 클래스들을 만들어 준다.
- 하이버네이트 구현체를 사용하면 코드 생성기는 org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor를 사용하면 된다.
- 코드 생성기는 모든 엔티티 클래스를 찾아서
엔티티명_.java
형태의 클래스를 생성해준다.
엔티티 -> 코드 자동 생성기 -> 메타 모델 클래스
src/jpabook/domain/Member.java // 원본 코드
target/generated-sources/annotations/jpabook/domain/Member_.java // 자동 생성된 메타 모델
10.4 QueryDSL
- QueryDSL -> 쿼리를 문자가 아닌 코드로 작성해도, 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는 프로젝트
- 쿼리를 자바코드로 작성할 수 있게 도와주는 기술이다.
- Spring Data JPA로 해결하지 못하는 복잡한 쿼리/동적 쿼리를 해결할 수 있다.
자바코드로 작성하기 때문에 문법오류를 컴파일 시점에 잡아낼 수 있다.
public void dsl() {
EntityManager em = emf.createEntityManager();
JPAQueryFactory query = new JPAQueryFactory(em);
QMember qMember = new QMember("m");
List<Member> result = query.selectFrom(member)
.where(member.name.eq("member1"))
.orderBy(member.name.desc())
.list(qMember);
}
- 엔티티 매니저를 생성자에 넘겨주고, 쿼리 타입(Q)을 생성하는데 생성자에는 별칭을 주면 된다.
- 이 별칭으로 사용한다.
select m from Member m
where m.name = ?1
order by m.name desc
- 기본 Q 생성
- 쿼리 타입(Q)은 사용하기 편리하도록 예제 10.80과 같이 기본 인스턴스를 보관하고 있다.
- 예제 10.80 Member 쿼리 타입
-
public class QMember extends EntityPathBase<Member> { public static final QMember member = new QMember("member1"); }
-
- 예제 10.81 쿼리 타입 사용
QMember qMember = new QMember("m"); //직접 지정 QMember qMember = new QMember.member // 기본 인스턴스 사용
- QueryDSL 기본 쿼리 기능
public void searchAndParam() { List<Member> result1 = queryFactory .selectFrom(member) .where(member.username.eq("member1"), member.age.eq(10)) }
- QueryDSL 검색 조건 쿼리
JPAQuery query = new JPAQuery(em); QItem item = QItem.item;
List list = query.from(item)
.where(item.name.eq("좋은상품").and(item.price.gt(200000)))
.list(item); //조회할 프로젝션 지정
* 실행 쿼리
* QueryDSL의 where 절에는 and나 or을 사용할 수 있다.
```sql
select item
from Item item
where item.name = ?1 and item.price > ?2
- 결과 조회
- 보통 uniqueResult()나 list()를 사용하여 대상을 조회 결과 조회 API는 com.mysema.query.Proejctable에 정의
- uniqueResult() : 조회 결과가 한 건일 때 사용 조회 결과가 없으면 null을 반환하고 결과가 하나 이상이면 NonuniqueResultException 발생
- singleResult() : uniqueResult()와 동일, 결과가 하나 이상이면 처음 데이터 반환 (fetchOne)
- list() :결과가 하나 이상일 떄 사용, 결과가 없으면 빈 컬렉션 반환 (fetch)
페이징과 정렬
- 정렬은 orderBy를 사용하고 쿼리 타입(Q)이 제공하는 asc(), desc()를 사용
- 페이징은 offset과 limit을 적절히 조합해서 사용
- restrict() 메소드에 QueryModifers를 파라미터를 사용해도 동일 작동
- 실제 페이징 처리를 하려면 검색 데이터 수가 필요 그럴 경우 listResults()를 사용
-
SearchResults<Item> result = query.from(item) .where(item.price.gt(20000)) .offset(10).limit(20) //offset은 시작점, limit는 가져오는 갯수 .listResults(item) //fetchResults로 메소드가 변경 됌
-
- 그룹은 groupBy를 사용하고, 그룹화된 결과를 제한할 경우 having을 사용
query.from(item) .groupBy(item.price) .having(item.price.gt(1000)) .list(item);
- 조인
- 조인은 innerJoin(join), leftJoin, rightJoin, fullJoin 사용 가능 추가로 JPQL의 on과 성능 최적화를 위한 fetch 조인도 사용 가능
- 기본 문법은 첫 번째 파라미터엔 조인 대상, 두 번째 파라미터엔 별칭으로 쓸 쿼리 타입 지정
//기본 조인 QOrder order = QOrder.order; QMember member = QMember.member; QOrderItem orderItem = QOrderItem.orderItem;
query.from(order)
.join(order.member, member)
.leftJoin(order.orderItems, orderItem)
.list(order);
//조인 on 사용
query.from(order)
.leftJoin(order.orderItems, orderItem)
.on(orderItem.count.gt(2))
.list(order);
//페치 조인 사용 방법
query.from(order)
.innerJoin(order.member, member).fetch()
.leftJoin(order.orderItems, orderItem).fetch()
.list(order);
//세타 조인 방법
query.from(order, member)
.where(order.member.eq(member))
.list(order);
* 서브 쿼리
* 서브 쿼리는 JPASubQuery를 생성해서 사용
* 서브 쿼리의 결과가 하나면 unique(), 여러 건이면 list()를 사용
```java
//서브쿼리 - 한건
QItem item = QItem.item;
QItem itemSub = new QItem("itemSub");
query.from(item)
.where(item.price.eq(
new JPASubQuery().from(itemSub).unique(itemSub.price.max())
))
.list(item);
//서브쿼리 - 여러건
query.from(item)
.where(item.in(
new JPASubQuery().from(itemSub)
.where(item.name.eq(itemSub.name))
.list(itemSub)))
.list(item);
- 프로젝션: select 절에 조회 대상을 지정하는 것을 의미
- 프로젝션 대상이 하나
QItem item = QItem.item; List<String> result = query.from(item).list(item.name);
for (String name : result) {
System.out.println("name = " + name;
}
* 여러 컬럼 반환과 튜플
* 여러 필드를 선택하면 QueryDSL의 기본으로 Tuple이라는 Map과 비슷한 내부 타입 사용
* 조회 결과는 tuple.get() 메소드에 조회한 쿼리 타입을 지정
```java
QItem item = QItem.item;
List<Tuple> result = query.from(item).list(item.name, item.price);
for (Tuple tuple : result){
System.out.println("name = " + tuple.get(member.name));
System.out.println("age = " + tuple.get(member.age));
}
- DISTINCT
query.distinct().from(item)...
수정, 삭제 배치 쿼리
- QueryDSL도 수정, 삭제 같은 배치 쿼리를 지원
- JPQL 배치 쿼리와 같이 영속성 컨텍스트를 무시하고 데이터베이스를 직접 쿼리
- 수정 배치 쿼리
// 수정 배치 쿼리 QItem item = QItem.item; JPAUpdateClause updateClause = new JPAUpdateClause(em, item); long count = updateClause.where(item.name.eq("jpa 책")) .set(imte.price, item.price.add(100)) .execute();
// 삭제 배치 쿼리
QItem item = QItem.item;
JPADeleteClause deleteClause = new JPADeleteClause(em, item);
long count = deleteClause.where(item.name.eq("jpa 책"))
.execute();
## 네이티브 SQL
* JPQL은 표준 SQL이 지원하는 대부분의 문법과 SQL 함수를 지원 하지만, 특정 데이터베이스에 종속적인 기능을 지원하지 못하는 기능이 존재
* 특정 데이터베이스만 지원하는 함수, 문법, SQL 쿼리 힌트
* 인라인 뷰(From 절에서 사용하는 서브쿼리), UNION, INTERSECT
* 스토어드 프로시저
### 특정 데이터베이스에 종속적인 기능을 지원하는 방법
- 특정 데이터베이스만 사용하는 함수
- JPQL에서 네이티브 SQL 함수를 호출 가능
- 하이버네이트는 데이터베이스 방언에 종속적인 함수들을 정의 또한 직접 호출할 함수 정의 가능
- 특정 데이터베이스만 지원하는 SQL 쿼리 힌트
- 하이버네이트를 포함한 몇몇 JPA 구현체들이 지원
- 인라인 뷰(From 절에서 사용하는 서브쿼리), UNION, INTERSECT
- 하이버네이트는 지원하지 않지만 일부 JPA 구현체들이 지원
- 스토어드 프로시저
- JPQL에서 스토어드 프로시저를 호출 가능
- 특정 데이터베이스만 지원하는 문법
- 오라클의 CONNECT BY처럼 너무 종속된 SQL문법은 지원하지 않으므로 네이티브 SQL 사용
- JPA가 지원하는 네이티브 SQL과 JDBC API의 차이
- 네이티브 SQL을 사용하면 엔티티를 조회 가능하고 영속성 컨텍스트 기능 사용 가능
- JDBC API는 단순히 데이터만을 조회
### 네이티브 SQL 사용
- 엔티티 조회
- em.createNativeQuery(SQL, 결과클래스) 를 사용
- JPQL와 거의 비슷하지만 실제 DB SQL과 위치기반 파라미터만 지원하는 차이가 존재
- 가장 중요한 점은 네이티브 SQL로 SQL만 직접 사용할 뿐 나머지는 JPQL과 동일 조회한 엔티티도 영속성 컨텍스트에서 관리
- JPA는 위치 기반 파라미터만 지원하지만 하이버네이트는 이름 기반 파라미터 사용 가능
- 값 조회
```java
String sql = "SELECT ID, AGE, NAME, TEAM_ID " +
"FROM MEMBER WHERE AGE > ? ";
Query nativeQuery = em.createNativeQuery(sql).setParameter(1, 10);
- 결과 매핑 사용
- 엔티티와 스칼라 값을 함께 조회하는 것처럼 매핑이 복잡해지면 @SqlREsultSetMapping을 정의해서 결과 매핑으로 사용
String sql = "SELECT M.ID, AGE, NAME, TEAM_ID, I.ORDER_COUNT " + "FROM MEMBER M " + "LEFT JOIN " + " (SELECT IM.ID, COUNT(*) AS ORDER_COUNT " + " FROM ORDERS O, MEMBER IM " + " WHERE O.MEMBER_ID = IM.ID) I " + "ON M.ID = I.ID"; Query nativQuery = em.createNativeQuery(sql, "memberWithOrderCount");
- 엔티티와 스칼라 값을 함께 조회하는 것처럼 매핑이 복잡해지면 @SqlREsultSetMapping을 정의해서 결과 매핑으로 사용
- Named 네이티브 SQL
- Named 네이티브 SQL을 사용해서 정적 SQL 작성이 가능
Named 네이티브 SQL 정리
- 네이티브 SQL도 JPQL을 사용할때와 같이 Query, TypeQuery를 반환
- JPQL과 같이 네이티브 SQL을 사용해도 페이징 처리 API 적용이 가능
- 네이티브 SQL은 관리가 쉽지 않고 자주 사용하면 종속적인 쿼리가 증가하여 이식성이 저하
- 사용 우선순위 권장 : 표준 JPQL > 하이버에니트 JPA 구현체 > 네이티브 SQL
- 네이티브 SQL로 부족함을 느끼면 Mybatis나 스프링 프레임워크가 제공하는 SQL 매퍼를 고려
스토어드 프로시저
- 프로시저를 사용하려면 em.createStoredProcedureQuery 메소드에 프로시저 이름 입력
- registerStoredProcedureParameter 메소드를 사용해서 사용할 파라미터를 순서, 타입, 파라미터 모드 순으로 정의
- 파라미터 모드는 ParamterMode에 정의
객체지향 쿼리 심화
- 객체지향 쿼리와 관련된 다양한 고급 주제들
- 한 번에 여러 데이터를 수정하는 벌크 연산
- JPQL과 영속성 컨텍스트
- JPQL과 플러시 모드
벌크 연산
- 여러 건을 한번에 수정하거나 삭제하는 경우 벌크 연산을 사용
// update 벌크 연산
String qlString =
"update Product p " +
"set p.price = p.price * 1.1 " +
"where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(qlString)
.setParameter("stockAmount", 10)
.executeUpdate();
// delete 벌크 연산
String qlString = "delete from Product p " +
"where p.price < :price";
int resultCount = em.createQuery(qlString)
.setParamter("price", 100)
.executeUpdate();
- 벌크 연산의 주의점
- 벌크 연산이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점을 주의
- 벌크 연산과 영속성 컨텍스트 값이 안맞는 현상 처리 방법
- m.fresh() 사용
- 벌크 연산 후 정확한 엔티티를 사용해야한다면 em.refresh()를 사용해서 다시 조회
- 벌크 연산 먼저 실행
- 벌크 연산을 가장 먼저 실행 후 엔티티를 조회, 이 방법은 JPA와 JDBC를 함께 쓸때도 유용
- 벌크 연산 수행 후 영속성 컨텍스트 초기화
- 벌크 연산을 수행한 직후 영속성 컨텍스트를 초기화해서 엔티티를 제거하는 방법 영속성 컨텍스트를 초기화했기 때문에 벌크 연산이 적용된 엔티티를 조회
- m.fresh() 사용
JPQL로 조회한 엔티티와 영속성 컨텍스트
- JPQL로 데이터베이스에 조회한 엔티티가 영속성 컨텍스트에 존재하면 데이터베이스에 조회한 결과를 버리고 영속성 컨텍스트에 있던 엔티티를 반환
em.find(Member.class, "member1"); //회원1 조회 // 엔티티 쿼리 조회 결과가 회원1, 회원2 List<Member> resultList = em.createQuery("select m from Member m", Member.class) .getResultList();
- JPQL로 조회한 새로운 엔티티를 추가하거나 대체할 때 생기는 문제
- 엔티티를 추가하거나 대체할 수 있는 3가지 방법
- 새로운 엔티티를 영속성 컨텍스트에 하나 더 추가
- 영속성 컨텍스트는 기본 키 값을 기준으로 하기 때문에 사용 X
- 기존 엔티티를 새로 검색한 엔티티로 대체
- 언뜻 보면 합리적이지만, 영속성 컨텍스트에 수정중인 데이터가 사라지는 위험 발생
- 기존 엔티티는 그대로 두고 새로 검색한 엔티티를 제거
- 영속성 컨텍스트는 엔티티의 동일성을 보장하므로 3번으로 동작
- 새로운 엔티티를 영속성 컨텍스트에 하나 더 추가
find() vs JPQL
- em.find()
- 1차 캐시를 통해 영속성 컨텍스트에 있으면 메모리에서 찾으므로 성능상 이점이 존재
//최초 조회, 데이터베이스 조회
Member member1 = em.find(Member.class, 1L);
//두 번째 조회, 영속성 컨텍스트에 있으므로 데이터베이스 조회X
Member member2 = em.find(Member.class, 1L);
- JPQL
- JPQL은 항상 데이터베이스에 SQL을 실행해서 결과를 조회
- 영속성 컨텍스트에 값이 존재해도 쿼리 후 결과를 버리고 영속성 컨텍스트 값을 반환
//첫 번째 호출: 데이터베이스 조회
Member member1 =
em.createQuery("select m from Member m where m.id = :id", Member.class)
.setParameter("id", 1L)
.getSingleResult();
//두 번째 호출: 데이터베이스 조회
Member member1 =
em.createQuery("select m from Member m where m.id = :id", Member.class)
.setParameter("id", 1L)
.getSingleResult();
- em.find()은 영속성 컨텍스트에서 엔티티를 먼저 찾고, JPQL은 데이터베이스를 먼저 조회
- JPQL의 특징 정리
- JPQL은 항상 데이터베이스를 조회
- JPQL로 조회한 엔티티는 영속 상태
- 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환
JPQL과 플러시 모드
- JPA는 플러시가 일어날 때 영속성 컨텍스트에 등록, 수정, 삭제 엔티티를 찾아 SQL 문을 만들어 데이터베이스에 반영
- flush() 직접사용 또는 플러시 모드에 따라 커밋 직전이나 쿼리 실행시 플러시가 호출
```java em.setFlushMode(FlushModeType.AUTO); //커밋 또는 쿼리 실행시 플러시(기본값) em.setFlushMode(FlushModeType.COMMIT); //커밋시에만 플러시 ``` - FlushModeType.COMMIT 모드는 성능 최적화를 위해 꼭 필요할 때만 사용
쿼리와 플러시 모드
- JPQL은 영속성 컨텍스트에 데이터를 고려하지 않고 데이터베이스에 데이터를 조회
- JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영이 필요
//가격을 1000 -> 2000원으로 변경 (변경 감지 사용)
product.setPrice(2000);
//가격이 2000원인 상품 조회
Product product2 =
em.createQuery("select p from Product p where p.price = 2000",
Product.class)
.getSingleResult();
- 플러시 모드가 AUTO이므로 쿼리 실행 직전에 영속성 컨텍스트가 플러시되므로 2000원으로 수정한 상품으로 조회
- 플러시 모드를 COMMIT으로 설정하면 수정한 데이터 조회가 불가능
- em.flush 또는 createQuery에 setFlushMode를 AUTO로 변경하면 조회 가능
em.setFlushMode(FlushModeType.COMMIT);
//가격을 1000 -> 2000원으로 변경 (변경 감지 사용)
product.setPrice(2000);
//1. em.flush() 직접 호출
Product product2 =
em.createQuery("select p from Product p where p.price = 2000",
Product.class)
.setFlushMode(FlushModeType.AUTO) //2. setFlushMode() 설정
.getSingleResult();
플러시 모드와 사용하는 이유 - (최적화)
- FlushModeType.COMMIT 모드는 트랜잭션을 커밋할때만 플러시 호출
- 못하면 무결성에 심각판 피해를 주지만, 플러시 횟수를 줄여 성능 최적화가 가능
//비즈니스 로직 등록() 쿼리() //플러시 등록() 쿼리() //플러시 등록() 쿼리() //플러시 커밋() //플러시
- FlushModeType.AUTO: 쿼리와 커밋할 때 총 4번 플러시
- FlushModeType.COMMIT: 커밋 시에만 1번 플러시
- JPA를 사용하지 않고 JDBC를 직접 사용할 때도 고민이 필요
- 별도의 JDBC 호출은 실행한 쿼리를 인식할 방법이 없음 JPA에 FlushModeType.AUTO 설정시 플러시가 발생X
- JDBC로 쿼리를 실행하기 직전에 em.flush()를 호출해 DB와 동기화하는 것이 안전
'스터디 > JPA 프로그래밍 스터디' 카테고리의 다른 글
JPA 프로그래밍 스터디 13장 정리 (0) | 2022.10.18 |
---|---|
JPA 프로그래밍 스터디 9장 정리 (0) | 2022.10.06 |
JPA 프로그래밍 스터디 8장 정리 (0) | 2022.10.01 |
JPA 프로그래밍 스터디 7장 정리 (1) | 2022.10.01 |
JPA 프로그래밍 스터디 6장 정리 (0) | 2022.10.01 |