쿼리를 Java코드와 같이 짤 수 있어 쿼리에 문제가 있을 시 컴파일 에러로 잡을 수 있음
단순하고 쉬우며 동적쿼리 작성도 편함
실용적으로 활용 가능해 실무에서 사용 가능
네이티브SQL
Sting으로 된 진짜 SQL 쿼리를 날려서 사용하는 방법
JPQL 해결이 안될 때 사용할 수 있지만 아래 다른 라이브러릴 활용이 일반적으로 더 나음
JDBC API 직접 사용, MyBatis, SpringJdbcTemplate
네이티브 SQL 처럼 JPQL 처럼 해결이 되지 않는 경우 활용할 수 있는 방법
주의 점: 영속성 컨텍스트가 반영되어 있지 않으면 결과가 달라질 수 있으므로 JPA와 함께 사용할 경우에는 SQL 실행 전 영속성 컨텍스트를 수동 플러시 해줘야함(JPQL은 날리면 그 전에 자동 플러시됨)
JPQL
JPA를 사용하면 DB를 엔티티 객체 중심으로 개발 가능
모든 DB 데이터를 객체로 변환해서 검색은 불가능
필요한 데이터만 뽑아 오려면 검색 조건이 포함된 SQL이 필요
JPA는 SQL을 추상화한 JPQL 이라는 객체 지향 쿼리 언어 제공
SQL과 유사하며 SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN을 기본 지원
JPQL은 엔티티 객체를 대상으로 쿼리
SQL은 데이터베이스 테이블을 대상으로 쿼리
특정 DB SQL에 의존하지 않는 객체 지향 SQL
JPQL 기본 문법
select m from Member as m where m.age > 18
엔티티와 속성은 대소문자 구분 필요(Member, age)
JPQL 키워드는 대소문자 구분 필요 없음(SELECT, FROM, where)
엔티티 이름을 사용하고, 테이블 이름을 사용하는게 아님
별칭은 필수(m) (as는 생략 가능)
집합과 정렬
기본적인 COUNT, SUM, AVG, MAX, MIN 제공
GROUP BY, HAVING, ORDER BY 또한 제공
TypeQuery, Query
TypedQuery<Member> query = entityManager.createQuery(SELECT m FROM Member m, Member.class);
List<Object[]> results = query.getResultList();
Query query = entityManager.createQuery(SELECT m.name, m.age FROM Member m);
Double averageAge = query.getSingleResult();
TypeQuery: 반환 타입이 명확할 때 사용
Query: 반환 타입이 명확하지 않을 때 사용
결과 조회: getResultList, getSingleResult
TypedQuery<Member> query = entityManager.createQuery(SELECT m FROM Member m, Member.class);
List<Object[]> results = query.getResultList();
Query query = entityManager.createQuery(SELECT m.name, m.age FROM Member m);
Double averageAge = query.getSingleResult();
query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
결과과 없으면 빈 리스트 반환(널 포인터 에러 나지 않음)
query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환
하나가 아니면 에러가 나는 상황이 발생할 수 있음
결과가 없으면: javax.persistence.NoResultException
둘 이상이면: javax.persistence.NonUniqueResultException
스프링 데이터 JPA 에서는 이를 감싸서 에러가 나지 않도록 함
파라미터 바인딩: 이름 기준, 위치 기준
//이름 기준
SELECT m FROM Member m where m.username=:username
query.setParameter("username", usernameParam);
//위치 기준
SELECT m FROM Member m where m.username=?1
query.setParameter(1, usernameParam);
이름 기준
이름에 맞는 파라미터 넣어 주는 방식
일반적으로 이 방식을 사용
위치 기준
위치 번호에 따라 파라미터 맵핑
중간에 새로운 파라미터 넣으면 숫자를 뒤로 다 미뤄야 해서 잘 사용하지 않음
프로젝션
SELECT 절에 조회할 대상을 지정하는 것
프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문다 등 기본 데이터 타입)
SELECT m FROM Member m -> 엔티티 프로젝션
SELECT m.team FROM Member m -> 엔티티 프로젝션
SELECT m.address FROM Member m -> 임베디드 타입 프로젝션
SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
//Query 타입으로 조회
Query query = entityManager.createQuery("SELECT m.name, m.age FROM Member m");
List<Object[]> results = query.getResultList();
//Object[]로 조회
TypedQuery<Object[]> query = entityManager.createQuery("SELECT m.name, m.age FROM Member m", Object[].class);
List<Object[]> results = query.getResultList();
//new 명령어로 조회
TypedQuery<MemberDTO> query = entityManager.createQuery(
"SELECT new MemberDTO(m.name, m.age) FROM Member m", MemberDTO.class);
List<MemberDTO> results = query.getResultList();
여러 값 조회
Query 타입으로 조회
Object[]로 조회
new DTO로 조회
장점
타입 안정성과 가독성이 뛰어남
반환된 DTO는 바로 사용 가능하므로 추가 형변환이 필요 없음
코드 유지보수가 좋음
단점
JPQL 쿼리 작성할 때 DTO의 생성자를 정확히 맞춰 작성해야 함
페이징
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();
DB마다 페이징 쿼리가 매우 다르고 특정 DB에서는 매우 복잡할 수 있는데 JPA는 손쉽게 사용 가능
setFirstResult(int startPosition): 조회 시작 위치(0 부터)
setMaxResults(int maxResult): 조회할 데이터 수
JOIN
//내부 조인
SELECT m.name, o.amount FROM Members m INNER JOIN Orders o
//외부 조인
SELECT m.name, o.amount FROM Members m LEFT OUTER JOIN Orders o
//세타 조인
SELECT m.name, o.amount FROM Members m, Orders o WHERE m.member_id = o.member_id;
//ON과 WHERE 차이
SELECT m.name, o.amount FROM Members m INNER JOIN Orders o ON m.member_id = o.member_id;
SELECT m.name, o.amount FROM Members m INNER JOIN Orders o WHERE m.member_id = o.member_id;
내부 조인( SELECT m FROM Member m [INNER] JOIN m.team t)
그냥 JOIN만 쓰면 inner 조인
JOIN 오른쪽 테이블에 매칭되는 데이터가 없으면 데이터가 안나옴
외부 조인( SELECT m FROM Member m LEFT [OUTER] JOIN m.team t)
LEFT JOIN 이라고 사용하면 LEFT OUTER JOIN
JOIN 오른쪽 테이블에 매칭되는 데이터가 없어도 null로 채워서 다 나옴
RIGHT JOIN 이라고 사용하면 RIGHT OUTER JOIN
JOIN 왼쪽 테이블에 매칭되는 데이터가 없어도 null로 채워서 다 나옴
세타 조인( select count(m) from Member m, Team t where m.username = t.name)
JOIN을 사용하지 않고 , 로만 사용
왼쪽 테이블 X 오른쪽 테이블로 다 일단 다 불러온 후 where절로 처리
쿼리 복잡도와 성능 이슈로 일반적으로 잘 사용하지 않고 테스트용으로 사용
ON 절 사용(JPA 2.1부터 지원)
연관관계가 맵핑되어 있으면 PK로 자동 ON을 수행함
WHERE 절은 JOIN 이후에 필터링을 하는 것이고, JOIN 절은 JOIN 중에 필터링 하는 것
따라서, 결과가 달라지며 상황에 맞게 사용해야 함
예시
SELECT m, t FROM Member m LEFT JOIN m.team t ON t.name = 'A'
SELECT m, t FROM Member m LEFT JOIN m.team t WHERE t.name = 'A'
서브 쿼리
//팀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)
서브쿼리 지원 함수
[NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
{ALL | ANY | SOME} (subquery)
ALL 모두 만족하면 참
ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
[NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능한데, 하이버네이트에서는 SELECT 절 지원
서브 쿼리를 원래 FROM절에는 지원이 안되었는데 하이버네이트 6부터 지원
원래 조인으로 풀 수 있으면 조인으로 풀어서 해결함
JPQL 표현 타입
// JPQL 예시 코드
String jpql = "SELECT m FROM Member m WHERE " +
"m.name = 'HELLO' AND " +
"m.description = 'She''s' AND " +
"m.age = 10L AND " +
"m.salary = 10D AND " +
"m.height = 10F AND " +
"m.isActive = TRUE AND " +
"m.type = jpabook.MemberType.Admin AND " +
"TYPE(m) = Member";
// 엔티티 매니저를 사용해서 쿼리 실행
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
문자: 'Hello', 'She''s'
숫자: 10L(Long), 10D(Double), 10F(Float)
Bolean: TRUE, FALSE
ENUM: jpabook.MemberType.Admin(패키지명도 들어가야 함)
엔티티 타입: TPYE(m) = Member(상속 관계에서 사용)
JPQL 기타 문법
String jpql = "SELECT m FROM Member m " +
"WHERE EXISTS (SELECT t FROM Team t WHERE t = m.team) AND " +
"m.age IN (20, 30, 40) AND " +
"m.salary > 50000 AND " +
"m.experience >= 5 AND " +
"m.rank < 10 AND " +
"m.joinDate <= :joinDate AND " +
"m.status <> 'inactive' AND " +
"m.score BETWEEN 50 AND 100 AND " +
"m.notes LIKE '%experienced%' AND " +
"m.manager IS NOT NULL AND " +
"NOT (m.retired = TRUE)";
SQL과 문법이 같은 식
EXISTS, IN
AND, OR, NOT
=, >, >=, <, <=, <>
BETWEEN, LIKE, IS NULL
조건식 - CASE, COALESCE, NULLIF
//기본 CASE 식
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
//단순 CASE 식
select
case t.name
when '팀A' then '인센티브110%'
when '팀B' then '인센티브120%'
else '인센티브105%'
end
from Team t
//COALESCE(사용자 이름이 없으면 이름 없는 회원을 반환)
select coalesce(m.username,'이름 없는 회원') from Member m
//NULLIF(사용자 이름이 '관리자'면 null 반환, 나머지는 본인 이름 반환
select NULLIF(m.username, '관리자') from Member m
select function('group_concat', i.name) from Item i
경로 표현식
점을 찍어 그래프를 탐색하는 것을 의미
상태 필드
단순 값 저장을 위한 필드 (m.username)
경로 탐색이 한 번 하면 무조건 끝
연관 필드
단일 값 연관 필드
@ManyToOne, @OneToOne, 대상이 엔티티인 경우(m.team)
묵시적으로 내부 조인 발생하며 탐색 O
컬렉션 값 연관 필드
@OneToMany, @ManyToMany, 대상이 컬렉션(m.orders)
묵시적 내부 조인 발생하며 탐색X
FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
명시적 조인, 묵시적 조인
명시적 조인: join 키워드를 직접 사용
select m from Member m join m.team t
묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생
select m.team from Member m
위오 같은 쿼리를 하면 team이 연관 필드이기 때문에 묵시적 내부 조인이 자동으로 발생
예제
select o.member.team from Order o -> 성공
멤버는 엔티티이므로 내부로 계속 탐색 가능
하지만 조인에 조인이 일어나는 문제
select t.members.username from Team t -> 실패
members는 컬렉션이므로 컬렉션 내부로는 탐색 불가능
select m.username from Team t join t.members m -> 성공
컬렉션을 별도의 조인을 명시적으로 하면 내부로 접근 가능
묵시적 주의 사항
항상 내부 조인으로 실행됨
컬렉션은 명시적 조인을 통해 별칭을 얻어서 접근해야함
컬렉션이 아니라 엔티티라도 묵시적 조인은 예기치 못한 조인이 계속 일어나므로 명시적 조인을 사용해야 함
묵시적 조인은 SQL의 FROM (JOIN) 절에 영향을 주므로 명시적으로 해줘야 유지보수가 좋음
추후 성능 향상에 영향을 주는게 JOIN인데 묵시적 조인이면 이를 개선하기가 어려움
페치 조인
//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
SQL의 기본 조인 종류는 아님
JPQ에서 성능 최적화를 위해 제공하는 기능
연관 맵핑된 엔티티나 컬렉션을 SQL 조회 시 한꺼번에 조회할 때 사용
위 예시처럼, JPQL에서 join fetch를 하면 아래 SQL처럼 전부 조회해서 나오게 됨
또한, 이 때 team의 정보를 즉시 로딩으로 바로 불러옴
페치 조인과 일대다 관계
//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'
일대다 관계에서 페치 조인시 join fetch 좌측에 있는 테이블은 결과값에 여러개가 들어갈 수 있음.
따라서, 결과를 출력하면 원치 않게 여러번 출력이 되는 문제가 발생할 수 있음.
DISTINCT
이는 기존 SQL의 DISTINCT로도 해결이 되지 않는데 그 이유는 오른쪽 테이블의 PK는 다르기 때문
JPA의 DISTINCT는 기존 SQL DISTINCT의 기능 + 애플리케이션에서 엔티티 중복 제거를 진행함
즉, 위 예제에서 팀A가 2개가 때문에 하나를 제거해줌.
페치 조인과 일반 조인의 차이
//JPQL
select t
from Team t join t.members m
where t.name = ‘팀A'
//SQL
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
일반 조인시 member의 내용도 모두 select는 하지만, 실제 해당 테이블을 조회한 것은 아니고 가짜로 생성만 해 놓음.
추후 필요한 시점에 불러서 사용하는 지연 로딩을 사용함
페치 조인의 즉시 조인과 다른 점
페치 조인의 특징과 한계
JPA에서 페치 조인 대상에는 별칭을 줄 수 없음
하이버네이트는 가능하지만 가급적 사용X
페치 조인은 연관된 테이블을 한 번에 로드해 그래프를 완성하는 것에 있는 것이지
페치 조인된 엔티티를 쿼리하는 것은 의미가 없음
둘 이상의 컬렉션은 페치 조인 할 수 없음
카르테시안 곱 문제 등으로 성능 문제 데이터 일관성 문제 등으로 허용하지 않음
컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없음
일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인을 해도 페이징이 가능
하지만, 일대다, 다대다와 같은 상황에서는 이전에 페치 조인과 일대다관계에서 설명했듯이 대상 테이블의 값이 여러개가 나오게 되므로 의도한 대로 페이징을 할 수 없음
문제는 하이버네이트는 이를 결고 로그만 남기고 메로리에서 페이징 하므로 매우 위험
페치 조인은 성능 최적화를 위해 실무에서 매우 자주 사용되는 기술
하지만, 모든 것을 페치 조인으로 해결할 수 는 없음
페치 조인은 객체 그래프를 유지할 때 사용하면 좋음
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면 페치 조인보다는 일반 조인을 사용하고, 필요한 데이터만 조회해서 DTO로 반환하는 것이 효과적
Type
//JPQL
select i from Item i
where type(i) IN (Book, Movie)
//SQL
select i from i
where i.DTYPE in (‘B’, ‘M’)
조회 대상을 특정 자식으로 한정
Item 중에 Book, Movie를 조회하라
type(i) 이런식으로 사용하면 Discriminator로 변환됨
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’
자바의 타입 캐스팅과 유사
상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
FROM, WHERE, SELECT(하이버네이트 지원) 사용
위 예제는 단일 엔티티 일때의 예제
엔티티 직접 사용 - 기본 키 값
//JPQL
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용
//SQL(JPQL 둘다 같은 다음 SQL 실행)
select count(m.id) as cnt from Member m
String jpql = “select m from Member m where m = :member”;
List resultList = em.createQuery(jpql)
.setParameter("member", member)
.getResultList();
String jpql = “select m from Member m where m.id = :memberId”;
List resultList = em.createQuery(jpql)
.setParameter("memberId", memberId)
.getResultList();
JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용
엔티티 직접 사용 - 외래 키 값
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();
String qlString = “select m from Member m where m.team.id = :teamId”;
List resultList = em.createQuery(qlString)
.setParameter("teamId", teamId)
.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();
미리 정의해서 이름을 부여해두고 사용하는 JPQL
동적은 안되고 정적 쿼리만 가능
어노테이션 또는 XML에 정의해서 사용
애플리케이션 로딩 시점에 초기화 후 재사용 가능
에플리케이션 로딩 시점이 쿼리를 검증하므로 컴파일 오류로 쿼리 문제 검출 가능
벌크 연산
// 특정 부서의 모든 직원의 급여를 10% 인상
String jpql = "UPDATE Employee e SET e.salary = e.salary * 1.1 WHERE e.department.name = :deptName";
int updatedCount = em.createQuery(jpql)
.setParameter("deptName", "Engineering")
.executeUpdate();