[JPA BASIC] 5. Query
in Jpa on Jpa
- Query 언어를 알아보자!
Query 언어를 알아보자!
JPA는 데이터를 관리하고 검색하는 데 있어서 강력한 기능을 제공하지만, 복잡한 쿼리를 처리하는 데에는 한계가 있을 수 있음. 이러한 한계를 극복하기 위해 JPA는 다양한 쿼리 방법을 지원하고 있다.
종류
- JPQL
- JPA Criteria
- QueryDSL
- 네이티브 SQL
- JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용
JPQL
- 테이블이 아닌 엔티티 객체를 대상으로 검색
- SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
- 동적 쿼리의 어려움: JPQL은 문자열 기반으로 쿼리를 작성하기 때문에, 동적 쿼리를 생성하는 과정에서 오류가 발생하기 쉽다. 이 때문에 복잡한 비즈니스 로직에 따라 쿼리를 조건적으로 변경해야 하는 상황에서는 코드가 복잡해지고 오류를 발견하기 어려울 수 있다.
- 별칭은 필수(m) (as는 생략가능)
//검색
String jpql = "select m From Member m where m.name like ‘%hello%'";
List<Member> result = em.createQuery(jpql, Member.class)
.getResultList();
Criteria
- 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
- JPQL 빌더 역할
- JPA 공식 기능
- 단점: 너무 복잡하고 실용성이 없다.
- Criteria 대신에 QueryDSL 사용 권장
//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();
QueryDSL
- 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
- JPQL 빌더 역할
- 컴파일 시점에 문법 오류를 찾을 수 있음
- 동적쿼리 작성 편리함
- 단순하고 쉬움
- 실무 사용 권장
//JPQL
//select m from Member m where m.age > 18
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;
List<Member> list =
query.selectFrom(m)
.where(m.age.gt(18))
.orderBy(m.name.desc())
.fetch();
네이티브 SQL 소개
- JPA가 제공하는 SQL을 직접 사용하는 기능
- JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능
- 예) 오라클 CONNECT BY, 특정 DB만 사용하는 SQL 힌트
String sql =
"SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList =
em.createNativeQuery(sql, Member.class).getResultList();
JDBC 직접 사용, SpringJdbcTemplate 등
- JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스등을 함께 사용 가능
- 단 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요
- 예) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시
JPQL - 기능
JPQL - 문법
- select m from Member as m where m.age > 18
- 엔티티와 속성은 대소문자 구분O (Member, age)
- JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)
- 엔티티 이름 사용, 테이블 이름이 아님(Member)
- 별칭은 필수(m) (as는 생략가능)
- COUNT(m), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age), GROUP BY, HAVING, ORDER BY 사용 가능
TypeQuery, Query
- TypeQuery: 반환 타입이 명확할 때 사용
- Query: 반환 타입이 명확하지 않을 때 사용
// TypedQuery
TypedQuery<Member> query =
em.createQuery("SELECT m FROM Member m", Member.class);
// Query
Query query =
em.createQuery("SELECT m.username, m.age from Member m");
결과 조회
- query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
- 결과가 없으면 빈 리스트 반환
- query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환(되도록 query.getResultList()을 쓰자)
- 결과가 없으면: javax.persistence.NoResultException
- 둘 이상이면: javax.persistence.NonUniqueResultException
바인딩
// 방법 1
Query query = entityManager.createQuery("SELECT m FROM Member m where m.username=:username");
query.setParameter("username", usernameParam);
// 방법 2
Query query = entityManager.createQuery("SELECT m FROM Member m WHERE m.username = ?1");
query.setParameter(1, usernameParam);
프로젝션 방법 - SELECT 절에 조회할 대상(컬럼)을 지정하는 것
- Query 타입으로 조회
- Object[] 타입으로 조회
- new 명령어로 조회
- 단순 값을 DTO로 바로 조회
- (SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m)
- 패키지 명을 포함한 전체 클래스 명 입력
- 순서와 타입이 일치하는 생성자 필요
- 단순 값을 DTO로 바로 조회
페이징 API
- JPA는 페이징을 다음 두 API로 추상화
- setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
- setMaxResults(int maxResult) : 조회할 데이터 수
//페이징 쿼리
String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setFirstResult(10)
.setMaxResults(20)
.getResultList();
조인
// 내부 조인:
SELECT m FROM Member m [INNER] JOIN m.team t
// 외부 조인:
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
// 세타 조인:
select count(m) from Member m, Team t where m.username = t.name
// ON절을 활용한 조인(JPA 2.1부터 지원)
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
// 서브 쿼리
select m from Member m where m.age > (select avg(m2.age) from Member m2)
서브 쿼리
- [NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
- {ALL | ANY | SOME} (subquery)
- ALL 모두 만족하면 참
- ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
- [NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
// 팀A 소속인 회원
select m from Member m where exists (select t from m.team t where t.name = '팀A')
// 전체 상품 각각의 재고보다 주문량이 많은 주문들
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)
서브 쿼리 한계
- JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능
- SELECT 절도 가능(하이버네이트에서 지원)
- FROM 절의 서브 쿼리는 JPQL에서 불가능
- 조인으로 풀 수 있으면 풀어서 해결
- 하이버네이트6 부터는 FROM 절의 서브쿼리를 지원한다
JPQL
- EXISTS, IN
- AND, OR, NOT
- =, >, >=, <, <=, <>
- BETWEEN, LIKE, IS NULL
case
- 기본 case 식 사용 가능
- COALESCE: 하나씩 조회해서 null이 아니면 반환
- NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
// 사용자 이름이 없으면 이름 없는 회원을 반환
select coalesce(m.username,'이름 없는 회원') from Member m
// 사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, '관리자') from Member m
기본 함수
- CONCAT
- SUBSTRING
- TRIM
- LOWER, UPPER
- LENGTH
- LOCATE
- ABS, SQRT, MOD
- SIZE, INDEX(JPA 용도)
사용자 정의 함수 호출
- 하이버네이트는 사용전 방언에 추가해야 한다.
- 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한 다.
select function('group_concat', i.name) from Item i
단일 값 연관 경로
묵시적 내부 조인(inner join) 발생, 탐색O (묵시적 조인 : join 키워드를 사용 안함 )
// JPQL:
select o.member from Order o
// SQL:
select m.*
from Orders o
inner join Member m on o.member_id = m.id
경로 표현식
select o.member.team from Order o // -> 성공
select t.members from Team // -> 성공
select t.members.username from Team t // -> 실패
select m.username from Team t join t.members m // -> 성공
경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인
- 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야함
- 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시 적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 줌
실무 조언
- 가급적 묵시적 조인 대신에 명시적 조인 사용
- 조인은 SQL 튜닝에 중요 포인트
- 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움
JPQL - 페치 조인
- SQL 조인 종류X
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
- join fetch 명령어 사용
- 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
// 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)
// SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT
// [JPQL]
select m from Member m join fetch 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) {
//페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X
System.out.println("username = " + member.getUsername() + ", " +
"teamName = " + member.getTeam().name());
}
컬렉션 페치 조인
// 일대다 관계, 컬렉션 페치 조인
// [JPQL]
select t
from Team t join fetch t.members
where t.name = '팀A'
// [SQL]
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
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);
}
}
페치 조인과 DISTINCT
- SQL의 DISTINCT는 중복된 결과를 제거하는 명령
- JPQL의 DISTINCT 2가지 기능 제공
- 1. SQL에 DISTINCT를 추가
- 2. 애플리케이션에서 엔티티 중복 제거(뻥튀기 데이터를 지워준다)
- 하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용된다.
select distinct t
from Team t join fetch t.members
where t.name = ‘팀A’
페치 조인과 일반 조인의 차이
- JPQL은 SELECT 절에 지정한 엔티티만 조회 한다.
- 페치 조인을 사용할 때는 연관된 엔티티도 함께 조회 한다. (즉시 로딩)
- 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념
페치 조인의 특징과 한계
- 페치 조인 대상에는 별칭을 줄 수 없다.
- 하이버네이트는 가능, 가급적 사용X
- 둘 이상의 컬렉션은 페치 조인 할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
- 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
- @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
- 실무에서 글로벌 로딩 전략은 모두 지연 로딩
- 최적화가 필요한 곳은 페치 조인 적용
일대다 관계 팁!
지연 로딩이나 일반 조인으로 데이터를 가져올때 한번에 in절을 사용하여 가져오는 방법
배치 사이즈란? 한 번에 로드할 수 있는 연관 데이터의 개수를 의미한다.
// 엔티티에서 직접 설정
@OneToMany
@BatchSize(size = 10)
private List<Order> orders = new ArrayList<>();
// properties
spring.jpa.properties.hibernate.default_batch_fetch_size=10
// yml
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 10
// 사용 예제
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
private Author author;
// getter와 setter 생략
}
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// getter와 setter 생략
}
public List<Book> findAllBooksWithAuthors() {
return entityManager.createQuery(
"SELECT b FROM Book b JOIN FETCH b.author", Book.class)
.getResultList();
}
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();
<!-- xml방법 -->
<!-- [META-INF/persistence.xml] -->
<persistence-unit name="jpabook" >
<mapping-file>META-INF/ormMember.xml</mapping-file>
<!-- [META-INF/ormMember.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>
벌크 연산
- 쿼리 한 번으로 여러 테이블 변경하는 방법 지원(ex : update … where p.age > 20)
- 벌크 연산은 데이터베이스에서 직접 진행하기 때문에 영속성 컨텍스트와 싱크가 안맞을 수 있다.
- 싱크 방법 두가지
- 1) 벌크 연산을 먼저 실행
- 2) 벌크 연산 후 clear() 후 다시 불러와 사용한다.
- 싱크 방법 두가지
// 벌크 연산
int resultCount = em.createQuery("update Member m set m.age = 20").executeUpdate();
// clear
em.clear();
// clear를 하지 않으면 반영 안된다.
Member findMember = em.find(Member.class, "id");