일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch4
- 게시글 목록 api
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch7
- 스프링 mvc1 - 스프링 mvc
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch9
- 스프링 입문(무료)
- @Aspect
- 스프링 고급 - 스프링 aop
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch2
- 스프링 mvc1 - 서블릿
- 스프링 db2 - 데이터 접근 기술
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch12
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch8
- 2024 정보처리기사 시나공 필기
- 2024 정보처리기사 수제비 실기
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch1
- 스프링 mvc2 - 타임리프
- 스프링 db1 - 스프링과 문제 해결
- Today
- Total
나구리의 개발공부기록
객체지향 쿼리 언어 - 중급 문법, 경로 표현식, 페치 조인(기본/한계), 다형성 쿼리, 엔터티 직접 사용, Named 쿼리, 벌크 연산 본문
객체지향 쿼리 언어 - 중급 문법, 경로 표현식, 페치 조인(기본/한계), 다형성 쿼리, 엔터티 직접 사용, Named 쿼리, 벌크 연산
소소한나구리 2024. 10. 18. 14:40출처 : 인프런 - 자바 ORM 표준 JPA 프로그래밍 - 기본편(유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 경로 표현식
1) 정의
- .(점)을 찍어서 객체 그래프를 탐색하는 것
- 상태필드, 단일 값 연관필드, 컬렉션 값 연관필드가 내부적으로 동작방식이 다르기 때문에 구분해서 이해해야함
select m.username # 상태 필드
from Member m
join m.team t # 단일 값 연관 필드
join m.orders o # 컬렉션 값 연관 필드
where t.name = '팀A'
2) 용어 정리
(1) 상태 필드(state field)
- 단순히 값을 저장하기 위한 필드
(2) 연관 필드(association field)
- 연관관계를 위한 필드
(3) 단일 값 연관 필드
- 다대일, 일대일 매핑이 되어 연관관계 대상이 엔터티인 필드
(4) 컬렉션 값 연관 필드
- 일대다, 다대다 매핑이 되어 연관관계 대상이 컬렉션인 필드
3) 특징
(1) 상태 필드
- 경로 탐색의 끝이기 때문에 해당 경로에서 다른 경로로 더이상 탐색이 불가능함
- 일반적인 조회쿼리가 나감
// 상태 필드 조회
String joinQuery = "select m.username from Member m";
List<String> resultList = em.createQuery(joinQuery, String.class).getResultList();
for (String s : resultList) {
System.out.println("m.username = " + s);
}
출력 결과
Hibernate:
/* select
m.username
from
Member m */ select
m1_0.username
from
Member m1_0
m.username = 관리자
m.username = 다른사람
(2) 단일 값 연관 경로
- 묵시적 내부 조인(inner join)이 발생하고 연관 필드에서 추가적인 탐색이 가능함
- 편해보이지만 가능하면 실무에서는 이렇게 쿼리를 작성하지 않는 것을 권장함
- 정말 작은 애플리케이션에서나 적용할만함
// 단일 값 연관필드 조회
// String joinQuery2 = "select m.team from Member m"; // 단일 값 연관필드 조회, join 발생
String joinQuery2 = "select m.team.name from Member m"; // 추가탐색이 가능함
List<String> resultList2 = em.createQuery(joinQuery2, String.class).getResultList();
for (String s : resultList2) {
System.out.println("m.username = " + s);
}
출력 결과 - inner 조인 발생
Hibernate:
/* select
m.team.name
from
Member m */ select
t1_0.name
from
Member m1_0
join
Team t1_0
on t1_0.id=m1_0.TEAM_ID
m.username = 팀A
m.username = 팀B
(3) 컬렉션 값 연관 경로
- 묵시적 내부 조인이 발생하지만 추가적인 탐색은 불가능함(size는 가능)
- from 절에서 명시적 조인을 통해 별칭을 얻으면 별칭으로 탐색이 가능함(명시적 조인을 사용)
// 컬렉션 값 연관필드 조회
String joinQuery3 = "select t.members from Team t";
// String joinQuery3 = "select t.members.size from Team t"; // 추가탐색은 size만 가능함
List<Collection> resultList3 = em.createQuery(joinQuery3, Collection.class).getResultList();
System.out.println("resultList3 = " + resultList3);
출력결과 inner join 발생
Hibernate:
/* select
t.members
from
Team t */ select
m1_0.id,
m1_0.age,
t1_0.id,
t1_0.name,
m1_0.type,
m1_0.username
from
Team t1_0
join
Member m1_0
on t1_0.id=m1_0.TEAM_ID
resultList3 = [Member{id=1, username='관리자', age=10}, Member{id=2, username='다른사람', age=10}]
4) 명시적 조인, 묵시적 조인
(1) 명시적 조인 - 권장 사항
- from 절에 join 키워드를 직접 사용함
- SQL과 동일한 문법으로 사용하기에 가독성이 좋아 유지보수하기가 좋음
(2) 묵시적 조인
- 경로 표현식에 의해 묵시적으로 SQL 조인 발생
- 항상 내부 조인이 발생됨
** 묵시적 조인 주의점
- 컬렉션은 경로 탐색의 끝이기 때문에 명시적 조인으로 별칭을 얻어야 추가 탐색이 가능함
- 경로 탐색은 주로 select, where 절에서 사용하지만 묵시적 조인으로 인해 SQL의 from 절에 영향을 주게 됨
(3) 예제
- 묵시적 조인과 명시적 조인 예제
- 실무에서는 명시적 조인을 사용하자
# 성공, 묵시적 조인으로 어떤 쿼리가 나올지 예측이 쉽지 않음
select o.member.team from Order o
select t.members from Team # 성공, 컬렉션으로 조회
select t.members.username from Team t #실패, 컬렉션은 추가적인 경로탐색이 불가능(size만 가능)
# 성공, 명시적조인으로 어떤 쿼리가 발생될지 예측이 가능함
select m.username from Team t join t.members m
** 참고
- 묵시적 조인 대신에 명시적 조인을 사용해야함
- 조인은 SQL 튜닝의 중요 포인트이기 때문에 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기가 어려워 최적화 및 유지보수에 어려움이 있음
- 묵시적 조인은 실무에서 아예 안쓰기로 협의하기도 함
** 실무에서 매우 중요함, 모르면 실무를 못할 정도라고함
2. 페치 조인(fetch join) - 기본
1) 정의
- SQL 조인의 종류가 아님
- JPQL에서 성능 최적화를 위해 제공하는 기능이며 연관된 엔터티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
- join fetch 명령어로 사용하며 [LEFT [OUTER] | INNER] JOIN FETCH 조인경로의 형식으로 작성함
2) 엔터티 페치 조인
- 회원을 조회하면서 연관된 팀도 SQL한번으로 조회함
- join fetch로 Member와 m.team을 조회하면 상태 필드를 m으로만 조회해도 멤버와 팀을 함께 조회함
# 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
(1) 예제 구조
- 회원은 3, 팀은 3개가 있음
- 회원1,2는 팀A에 소속, 회원3은 팀B에 소속, 회원4는 소속되어있지않고 팀C는 아무 회원이 없음
- 페치 조인을 사용하여 팀이 있는 회원을 조회하면 영속성 컨텍스트는 회원 1,2,3과 팀A, B 5개의 엔터티를 만들어 보관하고 반환함
(2) fetch join 미사용일 때 - N+1쿼리 문제 발생
// 엔터티 페치 조인 - 페치 조인 미적용
String fetchQuery1 = "select m from Member m";
List<Member> result = em.createQuery(fetchQuery1, Member.class).getResultList();
for (Member m : result) {
System.out.println("member = " + m.getUsername() + ", team = " + m.getTeam().getName());
}
- fetch join을 미적용하고 조회된 멤버에서 회원 이름과 팀을 조회하면 아래처럼 쿼리 로그가 보임
- jpql가 실행되어 회원을 가져오는 쿼리, 최초 반복문에서 회원의 이름과 팀의 이름을 조회하는 쿼리, 마지막 반복문에서 회원의 이름과 팀의 이름을 조회하는 쿼리 총 3개의 select 쿼리가 발생함
- 첫번째 반복문에서는 팀A에 대한 정보가 없어서 DB에서 team정보를 가져오는 쿼리가 발생하고, 2번째 반복문에서는 동일한 정보를 영속성 컨텍스트에서 관리하고 있기 때문에 캐시에서 정보를 반환함
- 세번째 반복문에서는 팀B에 대한 정보를 가지고 있지 않아서 다시 DB에서 team 정보를 가져오는 쿼리가 발생하게 되며 N+1 문제가 발생함
- 만약 조회해야할 값이 100개, 1000개이고 동시 접속자가 다수이면 심각한 성능저하가 발생할 수 밖에 없음
- 즉 LAZY(지연로딩)에서도 N+1문제는 발생할 수 밖에 없고 이를 해결하기 위해서는 fetch join이 가장 권장되는 해결 방법임
(2) fetch join 적용
// join fetch 적용
String fetchQuery1 = "select m from Member m join fetch m.team";
// ... 나머지 코드 동일
- 쿼리에 fetch join을 적용하고 동일한 코드를 실행하면 N+1문제가 해결되어 join 쿼리 한방으로 모든 데이터가 조회됨
- 프록시로 실행되는 것이 아니라 실제 DB에서 값을 조회한 값을 영속성 컨텍스트에 보관하고 반환하기 때문에 반복문으로 데이터를 조회해도 영속성 컨텍스트에 보관되어있는 값이 반환됨
- 실무에서 정말 많이 사용하는 방식이므로 꼭 알고 있어야 함
3) 컬렉션 페치 조인
- 일대다 관계인 컬렉션을 페치 조인하여 조회
(1) 예제 구조 - 하이버네이트 5 기준 설명이며 하이버네이트 6에서는 해당 문제가 해결됨 (아래 참고를 확인)
- 동일한 예제구조에서 팀과 회원을 페치 조인하여 조회하는 쿼리를 실행
- 일대다 관계(컬렉션 구조)에서는 페치 조인을 하게되면 조인이 실행된 DB에서는 팀A에 회원이 2개이므로 2개의 데이터를 반환함
- JPA는 DB에서 받은 두개의 데이터를 각각 독립적인 엔터티로 취급하여 동작하기 때문에 반환된 각 멤버를 팀A와 각각 객체를 생성하게 되고 팀A의 정보를 담은 객체가 2개가 생성되어버림
- 그리고 생성된 2개의 팀A정보를 담은 Team객체는 join된 정보로 회원정보를 찾기 때문에 팀A에 속한 회원은 2명을 조회하게됨
- 결국 최종 출력 결과는 팀A가 2번출력되고 팀B는 1번 출력됨
// 컬렉션 페치 조인
String fetchQuery2 = "select t from Team t join fetch t.members";
List<Team> result2 = em.createQuery(fetchQuery2, Team.class).getResultList();
for (Team team : result2) {
System.out.println("team = " + team.getName() + " | members = " + team.getMembers().size());
for (Member member : team.getMembers()) {
System.out.println(" -> member = " + member);
}
}
(2) 페치 조인과 DISTINCT
- 위의 문제를 해결하기위해 쿼리에 distinct를 붙이면 중복된 결과를 제거하여 팀A도 1번 팀B도 1번 출력되는 결과를 받을 수 있음
- SQL의 DISTINCT는 완벽하게 컬럼의 값들이 중복된 값만 제거하지만 JPQL에서의 DISTINCT는 추가로 애플리케이션에서 중복 제거를 시도하여 조회되는 엔터티의 같은 식별자를 가진 엔터티를 제거함
- 여기서는 동일한 ID값을 가진 Team 엔터티를 제거함
// distinct 적용
String fetchQuery2 = "select distinct t from Team t join fetch t.members";
** 참고 - 하이버네이트6 부터는 자동으로 DISTINCT를 추가하여 문제가 해결되었음
- 강의 시점에는 하이버네이트5였기 때문에 DISTINCT를 직접 추가해서 중복을 제거해줘야 했음
- 하이버네이트6 이후부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용됨
- 즉 일대다 연관관계(컬렉션)에서의 페치 조인에서 데이터를 조회해도 중복생성된 엔터티를 제거하기 때문에 원하는 결과를 바로 적용 받을 수 있음 (객체와 관계형 데이터베이스의 간극을 줄여줌)
- 그러나 과거 버전의 하이버네이트를 사용할 수도 있고, JPA 동작하는 원리와 해결했던 방식을 알고 있어야 미래에 대처를 할 수 있다고 생각하기에 전부 내용을 작성하였음
4) 페치 조인과 일반 조인의 차이
- JPQL은 결과를 반환할 때 연관관계를 고려하지 않고 단지 select 절에 지정한 엔터티만 조회함
- 즉, 일반 조인은 실행시 연관된 엔터티를 함께 조회하지 않기 때문에 여기에서는 팀 엔터티만 조회되고 회원 엔터티는 조회되지 않음
- 페치 조인을 사용하면 연관된 엔터티도 함께 조회(즉시 로딩처럼 동작함)하게됨
- 객체 그래프를 SQL한번에 조회하는 것처럼 동작함
- 페치 조인으로 실무에서의 대부분의 N+1쿼리 문제를 해결함
3. 페치 조인(fetch join) - 한계
1) 한계점
(1) 페치 조인의 대상에는 별칭을 사용하는 것을 권장하지 않음
- 페치 조인 대상에는 원칙적으로 별칭을 사용하여 추가적인 조작을 하지 않는것을 권장하고 있으며 관례적으로도 잘 사용하지 않음
- 페치 조인으로 쿼리를 하면 연관된 엔터티를 모두 가져오게 되는데 별칭을 사용해서 특정 조건을 주게 된다면 JPA가 의도한 설계 및 동작 메커니즘과 실제 동작이 맞지 않아 예상할 수 없는 쿼리가 발생할 수 있음
- 특정 대상으로 조회할 때는 전부 데이터를 가져왔는데, 또 어떤 상황에서는 동일 대상을 조회해도 일부만 가져오는 상황 자체를 영속성 컨텍스트가 모두 이해하여 동작하도록 설계되어있지 않기 때문에 데이터의 정합성에 문제가 발생할 가능성이 매우 높아짐
- 그래서 페치 조인으로 특정 조건을 명시하여 일부 데이터를 가져오고 싶은 상황이 발생할 수 있는데, 페치 조인에서 탐색해서 조회하는 것이 아니라 해당 데이터만 조회하는 쿼리를 별도로 만들어서 사용해야 함
- 유일하게 사용할 때는 fetch join을 단계적으로 적용하여 추가적인 엔터티를 한번에 가져와야할 때 정도만 사용을 한다고 볼 수 있으나 이 마저도 함부로 사용하면 위험함
- 하이버네이트도 이러한 상황을 알기에 여러 옵션을 제공하지만 해당 옵션을 사용하는 것은 실무에서 더욱 유지보수나 운영하기가 복잡하기 때문에 별도의 쿼리로 관리하는 것을 추천함
- 하이버네이트로 구현시에는 별칭을 붙혀도 에러가 발생되지는 않지만 위험하니 가급적 사용하지 말것을 권장함
- 본인이 JPA나 데이터를 잘 다루어 각 엔터티간의 데이터가 일관성이 깨지지 않는 선에서 사용할 수 있을 때에나 간단한 조회시에만 사용하자 - 김영한님은 성능 최적화를 위해 종종 사용한다고함
** 정합성
- 데이터의 정확성, 일관성, 신뢰성을 의미하는 중요한 개념이며 시스템 내의 모든 데이터가 일관되고 정확하며 유효한 상태를 유지하는 것을 의미함
- 즉, DB의 데이터가 실제 값을 정확히 반영하고 시스템이나 데이터베이스 간의 데이터 불일치가 없고 필요한 모든 데이터는 누락없이 존재해야하며 정의된 규칙과 제약 조건을 준수하는 것
(2) 둘 이상의 컬렉션은 페치 조인 할 수 없음
- 일대다대다의 연관관계가 적용되어 데이터의 정합성에 문제가 생길 가능성이 높음
- 적용은 가능하지만 사용하지 않는 것을 권장함
(3) 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없음
- 기존의 하이버네이트5에서는 일대다 관계시에서 fetch join시 데이터 뻥튀기(컬렉션 페치 조인 내용 참고)가 발생하기때문에 제대로 값을 가져오지 않는 문제가 있었으나 하이버네이트6에서는 자동으로 문제를 해결하므로 해당 문제는 발생하지 않음
- 그러나 적용해보면 하이버네이트가 경고 로그를 남기는데 메모리에서 페이징이 실행되기 때문에 장애가 발생하기 좋음
- 실행 후 쿼리로그를 보면 페이지네이션 관련 문법을 사용해 DB에서 특정 값만 가져오는 것이 아니라 DB데이터를 전부 가져오고 난 뒤에 메모리에서 페이징을 하는 것이므로 절대 사용하지 말아야함
// 실제 발생한 에러정보
WARN: HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
2) 컬렉션 페치 조인시 페이징 문제 해결 방법 소개
(1) 다대일 관계의 쿼리로 변경
- 조회 쿼리는 일대다가 아닌 다대일로 변경하여 페이지네이션을 적용하여 원하는 값을 가져올 수 있도록 쿼리를 작성하거나 비즈니스 로직 등을 변경
(2) @BatchSize를 적용
- 기존의 일대다 연관관계에서 일쪽인 엔터티의 연관관계 매핑필드에 @BatchSize를 적용하거나 글로벌 세팅으로 적용(실무에서는 글로벌 세팅으로 적용하여 사용함)
- persistence.xml에 propertis 속성에 아래처럼 지정하면 @BatchSize 애노테이션을 사용하지 않고도 글로벌로 적용할 수 있음
<!-- 배치 사이즈 관련 설정 -->
<property name="hibernate.default_batch_fetch_size" value="100"/>
- 조회쿼리에 join fetch를 적용하지 않고 글로벌 전략의 지연로딩(LAZY) 로딩만 적용한 상태에서 Team 클래스의 연관관계 매핑 필드에(일대다 관계중 일(1)쪽) @BatchSize(size = 1000이하의 값)을 적용
- BatchSize는 일반적으로 클 수록 성능 최적화에 좋지만 1000이하의 값 중 적절한 값으로 적용하면 됨(대부분의 DB에서의 IN절의 최대 항목수가 대부분 1000개이기 때문)
- 위처럼 해결하면 N+1문제가 테이블 수만큼만 추가 쿼리가 발생하는 것으로 완전히 해결하는 것이 아닌 완화하는 방법임
// 페이징 테스트2 - 코드 실행 부분
String pagingQuery2 = "select t from Team t";
List<Team> result = em.createQuery(pagingQuery2, Team.class)
.setFirstResult(0).setMaxResults(2)
.getResultList();
for (Team t : result) {
System.out.println("team = " + t.getName() + " | members = " + t.getMembers().size());
for (Member member : t.getMembers()) {
System.out.println(" -> member = " + member);
}
}
// Team 클래스에 @BatchSize 적용
@BatchSize(size = 300)
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
** 참고
- 강의에서는 배치사이즈를 늘려도 쿼리 로그의 결과에서 statement의 ?의 개수가 조회된 개수만큼 로그에서 보였는데, 지금은 배치사이즈만큼 log에서 찍히게 변경됨
- 이는 하이버네이트6.2에서 최적화하는 방식이 변경되었기 때문이며 실제 동작은 생성된 ?에서 필요한 값만 채우고 나머지는 null로 동작하는 방식으로 변경되었으며 최종적으로 호출되는 쿼리의 수는 동일함
(3) DTO로 뽑아서 직접 쿼리를 짜는 방법
- 정제해야할 것이 많아서 쉽게 적용할 수 있지는 않음
3) 페치 조인 정리
- 페치 조인은 연관된 엔터티들을 SQL한 번으로 조회하여 성능이 최적화되며 객체 그래프를 유지할 때 사용하면 효과적임
- 엔터티의 연관관계 매핑 시 직접 적용하는 글로벌 로딩 전략(지연로딩, 즉시로딩)보다 우선적으로 동작함
- 실무에서 글로벌 로딩 전략은 모두 지연로딩으로 하고 최적화가 필요한 곳에 페치 조인을 적용하는 것 방식으로 성능문제 해결을 접근해야 함(대부분의 문제는 N+1문제)
- 대부분의 문제를 페치 조인으로 해결할 수는 있으나 모든 것을 해결할 수는 없음
- 여러 테이블을 조인해서 엔터티가 가진 모양이 아니라 전혀 다른 결과가 필요하다면 처음 적용시부터 페치 조인보다는 일반 조인을 사용하여 필요한 데이터만 조회한 뒤 DTO로 반환하는 방법이 효과적일 수 있음
** 참고
- 페치 조인에 대해서는 매우 중요하기 때문에 100% 이해를 해야함
- 성능 최적화 문제를 네이티브 SQL로 변환하여 문제를 해결하는 것보다 페치 조인으로 해결하는 것이 수백배는 시간을 아낄 수 있음
- 성능 튜닝을 과거에는 직접 쿼리를 뜯어고치면서했지만 이렇게 선언적으로 성능 튜닝을 할 수 있게 됨
4. 다형성 쿼리 / 엔터티 직접 사용
1) 다형성 쿼리
(1) 구조 예시
- 엔터티가 Item의 자손으로 Album, Movie, Book이 있다고 가정
(2) TYPE 사용
- 조회 대상을 특정 자식으로 한정 지을 수 있음
- 상속관계인 대상 중 자식 엔터티를 입력하여 특정 자식의 데이터만 조회할 수 있음
# JPQL 작성 시
select i from Item i where type(i) IN (Book, Movie)
# SQL 발생 쿼리
select i from i where i.DTYPE in (‘B’, ‘M’)
(3) TREAT(JPA2.1 이후)
- 자바의 타입 캐스팅과 유사하며 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용함
- from, where, select에서 사용가능
- 상속관계인 대상 중 부모 타입을 자식타입으로 형변환 하는 것처럼 적용하여 자손의 필드를 접근하여 쿼리를 작성함
# 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’
2) 엔터티 직접 사용 시 동작
(1) 기본 키 값을 사용
- JPQL에서는 함수에 엔터티를 쿼리문에 직접 사용할 수 있는데 엔터티를 직접 사용하게되면 SQL에서는 작성된 엔터티의 기본 키 값을 사용하여 조회함
- 파라미터 바인딩으로 엔터티를 직접 넘겨도 SQL에서는 기본 키 값을 사용하여 조회함
- 엔터티가 아닌 기본키를 직접 입력하는 것도 가능하지만 객체지향적으로 엔터티를 입력하는 것이 좋을 수 있음
String entityQuery = "select m from Member m where m = :member";
Member findMember = em.createQuery(entityQuery, Member.class)
.setParameter("member", member1)
.getSingleResult();
System.out.println("findMember = " + findMember);
// 실행결과
Hibernate:
/* select
m
from
Member m
where
m = :member */ select
m1_0.id,
m1_0.age,
m1_0.TEAM_ID,
m1_0.type,
m1_0.username
from
Member m1_0
where
m1_0.id=?
findMember = Member{id=1, username='회원1', age=0}
(2) 외래 키 값을 사용
- 연관 관계의 엔터티를 직접 쿼리문에 사용하면 연관된 엔터티의 외래 키 값을 사용함
- 마찬가지로 직접 외래키의 값을 사용해도 됨
// 직접사용 - 외래 키 값 사용
String entityQuery = "select m from Member m where m.team = :team";
List<Member> resultList = em.createQuery(entityQuery, Member.class)
.setParameter("team", team1)
.getResultList();
for (Member member : resultList) {
System.out.println("member = " + member);
}
// SQL 결과 생략, 외래키인 TEAM_ID필드가 사용됨
5. Named 쿼리
1) 정의
- 미리 정의를 해두고 이름을 부여하여 사용하는 정적 JPQL 쿼리
- 애노테이션, XML에 정의할 수 있음
- 애플리케이션 로딩 시점에 초기화 후 재사용하며 로딩 시점에 쿼리를 검증함(장점)
- 즉 애플리케이션 로딩 시점에 오류를 확인할 수 있기 때문에 사용자가 애플리케이션을 사용하다가 오류를 발생시킬 확률을 줄여줌
2) 적용
(1) 애노테이션으로 적용
- 쿼리를 적용할 엔터티에 @NamedQuery를 사용하여 쿼리의 이름과 query를 작성
- 쿼리의 이름은 관례상으로 엔터티명.쿼리명 처럼 작성한다고 함
- em.createNamedQuery로 @NamedQuery에서 작성한 쿼리를 이름으로 호출할 수 있음
@Entity
@NamedQuery(
// 관례상 Entity명.네이밍 으로 많이 씀
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"
)
public class Member {
// ... 코드 생략
}
// 프로그램 실행
// named 쿼리
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", member1.getUsername())
.getResultList();
for (Member member : resultList) {
System.out.println("member = " + member);
}
// 실행 결과
// member = Member{id=1, username='회원1', age=0}
(2) XML에 정의
- 코드를 사용하는 방법은 같으며 persistence.xml의 mapping-file 세팅와 매핑할 파일의 위치를 적용하는 것에 주의하면 됨
- 애노테이션보다 XML이 항상 우선권을 가지며 애플리케이션 운영 환경에 따라 XML을 배포할 수 있고 여러 파일에 쿼리를 분리하여 각각 관리 및 등록할 수 있음
- persistence.xml에 네임드 쿼리를 작성한 xml파일의 위치를 <mapping-file> 속성을 사용하여 등록
- 매핑 파일 등록 시 <persistence-unit name="..."> 위치 바로 아래에 등록해야하며 다른곳에 등록하면 오류가 발생함
<persistence-unit name="jpqltest">
<!-- persistence-unit 바로 밑에 namedQuery를 정의한 xml파일을 매핑해야함, 다른곳에 하면 오류 발생함-->
<mapping-file>
META-INF/ormMember.xml
</mapping-file>
- 네임드 쿼리를 작성할 xml파일은 아래와 같이 작성하면 됨
- 여러개의 네임드 쿼리를 하나의 xml파일에 관리할 수 있음
<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>
** 참고
- 이후에 배울 스프링 데이터 JPA를 사용하면 NamedQuery를 엔터티가 아니라 DAO나 Repository에 @Query 애노테이션을 사용하여 NamedQuery의 기능을 적용할 수 있음
- 실무에서는 네임드쿼리를 직접사용하지않고 스프링 데이터 JPA를 사용하기 때문에 엔터티나 별도의 XML로 쿼리를 관리하지 않아도 DB와 소통하는 곳에서 쿼리를 한번에 관리할 수 있음
- 즉 스프링 데이터 JPA도 이 @NamedQuery를 파싱하여 동작하기 때문에 원래의 동작 방식을 이해해둘 필요성은 있다고 봄
6. 벌크 연산
- 쿼리 한번으로 여러 테이블(엔터티)의 데이터를 변경하는 것
1) 벌크연산 미적용 시 예시
- 만일 수많은 상품이 데이터베이스에 저장되어있다고 가정
- 재고가 10개 미만인 모든 상품의 가격을 10% 상승하고자 할때 JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL이 실행되어야 함
- 재고가 10개 미만인 상품을 리스트로 조회하고, 조회된 상품 엔터티의 가격을 10% 증가하고 트랜잭션 커밋 시점에 변경감지가 동작하여 DB에 반영되어야 함
- 만약 변경된 데이터가 천건, 만건 이라면 그 횟수만큼의 updateSQL이 발생됨
2) 벌크 연산 예제
- update, delete는 기본으로 지원하고, 하이버네이트로 구현시에는 insert도 구현할 수 있음
- executeUpdate()메서드로 작성한 JPQL을 벌크 연산으로 실행할 수 있으며 연산 결과는 영향을 받은 로우의 수를 반환함
- 회원의 나이를 모드 20으로 설정하는 쿼리를 작성해보면 정상적으로 update쿼리가 생성되고 결과가 DB에 반영됨
// 벌크 연산 - update
int resultCount = em.createQuery("update Member m set m.age = 20").executeUpdate();
System.out.println("resultCount = " + resultCount);
3) 벌크 연산 주의점
- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 실행하기 때문에 잘못하면 꼬일 수 있음
(1) 문제 해결 방법
- 영속성 컨텍스트와 관계없이 벌크 연산을 독립적으로 먼저 실행 후 커밋
- 벌크 연산을 수행 후 영속성 컨텍스트를 초기화
(2) 주의점 예제
- 데이터 생성 후 벌크연산으로 쿼리를 실행하면 JPA 기본 동작(commit, 쿼리를 직접 날릴 때, em.flush로 직접 호출할 때)으로 자동으로 플러시가 호출 되기때문에 데이터는 반영이되고, 반영된 데이터에 벌크연산의 쿼리가 수행 됨
- 그러나 벌크연산으로 수행된 이후 commit()이 되기 전에 코드로 데이터를 조회를 해보면 영속성 컨텍스트에 반영이 되어있지 않으므로 회원의 나이는 변경되기 전의 나이가 출력됨
// 벌크 연산 - update
int resultCount = em.createQuery("update Member m set m.age = 20").executeUpdate();
System.out.println("resultCount = " + resultCount);
// 영속성 컨텍스트의 데이터를 조회 -> 값이 없음
System.out.println("member1.getAge() = " + member1.getAge());
System.out.println("member1.getAge() = " + member2.getAge());
System.out.println("member1.getAge() = " + member3.getAge());
// 실행 결과
resultCount = 4
member1.getAge() = 0
member2.getAge() = 0
member3.getAge() = 0
- 영속성 컨텍스트를 무시하고 DB에 쿼리를 반영해버리기 때문에 벌크연산 이후에 추가적인 쿼리를 작성한다고 해도 모든 연산결과가 동작하지 않음
- 그래서 벌크 연산 이후에 em.clear()로 영속성 컨텍스트를 초기화 해주면 이전의 처음부터 시작하기 때문에 select쿼리가 수행되면서 값을 가져오는 것을 확인할 수 있음
- 이때 commit() 되기전에 외부에서 DB를 조회하면 DB에는 해당 값이 저장된 것을 확인할 수 없는데, 이는 트랜잭션의 격리성 때문에 commit이 되기 전까지 외부에서 데이터를 볼 수 없기 때문 - https://nagul2.tistory.com/307 내용 확인
// 기존 예제 코드 동일
em.clear(); // 영속성 컨텍스트 초기화
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember.getAge() = " + findMember.getAge());
// 실행 결과
Hibernate:
select
m1_0.id,
m1_0.age,
m1_0.TEAM_ID,
m1_0.type,
m1_0.username
from
Member m1_0
where
m1_0.id=?
findMember.getAge() = 20
** 참고
- 스프링 데이터 JPA를 사용하면 @Modifying으로 em.clear를 자동으로 수행하여 벌크 연산을 편리하게 사용할 수 있음
- 실무에서는 스프링 데이터 JPA를 사용할텐데, 구현의 근본은 JPA의 기본 구현을 원리로 구현했기 때문에 JPA의 기본 원리를 알고 실무를 접하는 것이 좋음