JPQL 의 개념과 간단한 사용 방법 ( 2 / 2 )
이전 포스팅에서 이어집니다.
JPQL 의 개념과 간단한 사용 방법 ( 1 / 2 )
게시글을 하나로 통합할까 하다가, 이론적인 부분과 실제 사용하는 구문을 분리하는 게 좋겠다는 생각이 들어서 두 개로 나눴다. 이 게시글은 JPQL 의 개념에 대한 내용이 주가 되었으니 참고 바
zigo-autumn.tistory.com
4. JPQL 에서의 페이징은 어떨까?
JPA 에서 페이징은 setFirstResult(in statPosition )과 setMaxResults(int maxResult)로 추상화할 수 있다.
각각의 API 의미는 다음과 같다.
- setFirstResult(int startPosition) : 조회 시작 위치
- setMaxResults(int maxResult) : 조회할 데이터 수
for (int i = 0; i < 100; i++) {
Member member = new Member();
member.setUsername("member" + i);
member.setAge(i);
em.persist(member);
}
em.flush();
em.clear();
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(1)
.setMaxResults(10)
.getResultList();
System.out.println("result.isze = " + result.size());
for (Member member1 : result) {
System.out.println("member1 = " + member1);
}
위 코드를 실행하면, 1번째 행(row) 부터 10개의 행이 조회됨을 알 수 있다. 실제 쿼리는 persistene.xml 파일의 hibernate.idalect 에 지정된 dbms에 맞게 페이징 쿼리가 자동으로 실행된다
실무에서 MariaDB(MySQL) 은 그렇다치고 Oracle을 사용할 때마다 코드가 길어지고 지저분해져서 보기 싫었는데 ( 항상 짤때마다 오라클 정도 되는 DBMS 가 왜 이렇게 페이징 지원에 야박한 지 원망함 ) 이 내용을 보고 정말 마음에 들었다.
5. 조인
조인은 내부조인, 외부조인, 세타조인이 있다. 이 부분은 사실 SQL 과 거의 유사하기 때문에 간단한 예제만 작성했다.
내부, 외부 조인은 연관관계가 있어야하고 세타조인은 연관관계가 없어도 아무렇게나 사용할 수 있다는 차이점이 있다.
- 내부 조인
String query = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
- 외부 조인
String query = "select m from Member m left join m.team t";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
- 세타조인
String query = "select m from Member m, Team t where m.username = t.name";
List result = em.createQuery(query, Member.class)
.getResultList();
ON 절 사용하기
- 조인 대상을 필터링 해준다.
- 연관관계없는 엔티티 외부 조인을 지원한다. ( 하버네이트 5.1부터)
서브쿼리
from 절의 서브 쿼리는 현재 jpql에서 불가능하다. (조인으로 풀 수 있으면 풀어서 해결하는데 풀어서 해결 안 되면 그냥 포기해야 한다 )
6. 기타 구문 - case, coalesce, nullif
- case
String query = "select " +
"case when m.age <= 10 then '학생요금'"
+ " when m.age >= 60 then '경로요금'"
+ " else '일반요금' end"
+ " from Member m";
List<String> result = em.createQuery(query, String.class).getResultList();
for (String s : result ) {
System.out.println(" s = " + s);
}
- coalesce ( 글자 합치기 )
String query = "select coalesce(m.username, '이름 없는 회원') from Member m";
List<String> result = em.createQuery(query, String.class).getResultList();
for (String s : result ) {
System.out.println(" s = " + s);
}
- nullif ( nullif( a, b )에서 a 가 null 이면 b 값으로 치환 )
String query = "select nullif(m.username, '관리자') from Member m";
List<String> result = em.createQuery(query, String.class).getResultList();
for (String s : result ) {
System.out.println(" s = " + s);
}
7. fetch join ( 페치 조인 )과 N + 1
N + 1 문제는 JPA 관련 게시글을 검색하면 정말 자주 따라오는 단골 키워드다.
N + 1 문제는 사용자가 데이터를 조회하기 위해 최초로 실행한 쿼리가 1번 , 그 쿼리로 인해서 발생하는 ( 원하지 않은 ) 쿼리가 N 번 발생하는 것을 의미한다.
아래 예시를 보자.
String query = "select m From Member m";
List<Member> result = em.createQuery(query, Member.class).getResultList();
for(Member me : result ) {
System.out.println("member = " + member.getUsername()
+ ", TeamNm : " + me.getTeam().getName()) ;
}
select m From Member m 까지는 큰 문제가 없다. 그런데 sysout으로 출력하는 과정에서, team의 내용이 필요하게 된다.
이때 내부적으로 팀을 조회하기 위한 쿼리가 team 의 이름을 조회할 때마다 발생하게 된다. 즉, team 의 개수를 N 개 라고하면 N 번의 추가 쿼리가 발생하게 되는 것이다.
사실 작은 데이터를 조회할 땐 큰 문제가 없겠지만, 이게 수십 건의 데이터가 엮인 실제 시스템에서 발생한다고 생각하면 끔찍한 일이 아닐 수 없다. 이럴 때 사용하는 것이 fetch join ( 이하 fetch join 혹은 페치 조인 )이다.
다음은 페치 조인을 사용하여 개선된 코드이다.
String query = "select m From Member m join fetch m.team";
List<Member> result = em.createQuery(query, Member.class).getResultList();
for(Member me : result ) {
System.out.println("member = " + member.getUsername()
+ ", TeamNm : " + me.getTeam().getName()) ;
}
사용자가 보는 결과는 ( 얻는 데이터는 ) 동일할지라도, 내부적으로는 단 하나의 쿼리만 실행되게 된다.
페치 조인은 일반 조인과 차이점을 보이는데 즉시 로딩이라는 점과 한 번에 연관된 엔티티를 모두 묶어서 가져온다는 것이다.
좋아 보이는 페치조인도, 한계점은 분명히 있다. 아래에서 기술하는 한계점은 실제로 코드가 실행되지 않는 것은 아니지만, 개발자가 원하는 결과 값이 도출되지 않으므로 오류로 취급한다.
첫 번째, 페치 조인 대상에는 별칭을 사용할 수 없다.
사실 별칭을 못 쓰는 것은 아니다. 문법적으로 오류가 나진 않는데 실행 결과가 일관성을 잃는다. 아래코드를 실행하면 members 가 부분적으로만 출력되는 문제가 발생한다. ( 이때 team와 members 은 OneToMany를 가짐 )
select t From Team t join fetch t, members as m
여기서 member as m 이 별칭에 해당한다. jpa는 members 가 다 출력된다는 가정하에 설계되었다는 걸 항상 염두에 두자.
둘째, 둘 이상의 컬렉션의 페치조인을 할 수 없다.
일대다(@OneToMany)에서도 페치조인을 사용하면 리턴되는 데이터 개수가 늘어나는데 컬렉션끼리 해버리면 개수가 한 번에 엄청나게 증가할 수 있다.
셋째, 컬렉션을 페치 조인하면 페이징 api를 사용할 수 없다
일대일(@OneToOne), 다대일(@ManyToOne) 같은 단일 값 연관 필드들은 페치조인을 해도 데이터가 증가하지 않아서 페이징 해도 문제가 없다. 만약 일대다가 문제가 될 경우에는, 다대일로 변경해서 사용하면 된다.
- 컬렉션 페치 조인 ( 일대다 관계일 때 조회하기, 팀 입장에서 멤버를 조회할 때 )
조인을 하게 되면 데이터가 늘어나는 경우가 있다. 이럴 때 사용하는 것이 distinct이다. distinct는 sql 단에서도 중복을 제거해 주고 리턴된 엔티티에 대해서도 중복을 제거해 주는 역할을 한다.
( Hibernate 6부터는 중복제거가 기본으로 들어간다 )
8. BatchSize
일대다(@OneToMany)에서 컬렉션에 대해서 쓸 수 있는 애노테이션으로 , 사용 시 비슷한 쿼리들을 묶어 주는 역할을 한다.
예를 들어 team ( 1 )에 있는 members ( N )를 조회할 때, N의 수를 예상해서 BatchSize를 지정해 주면 1 + N 이 발생하지 않고 IN을 사용해서 동일한 team을 가진 members를 한 번에 묶는 쿼리를 날려준다.
@BatchSize(size = 100 )
persistene.xml 파일에 글로벌 세팅으로 주고 사용할 수 있다.
<property name="hibernate.default_batch_fetch_size" value="100" />
9. 기타 - @named와 벌크 연산
- @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();
- 벌크연산 주의
벌크연산은 간단하게 설명하면, 데이터베이스에 쿼리 한 번을 실행해준다고 생각하면 된다. 주의해야 할 점은 영속성 콘텍스트를 건너뛰고 데이터베이스에 직접 접근해서 실행되기에, 연산 수행 후 영속성 컨텍스트를 초기화 ( em.clear() ) 해줘야 한다.
벌크연산 전의 데이터 ( 영속성 콘텍스트에 남아있는 데이터 )와 실행 후의 데이터 ( DB에 있는 값 )가 달라질 수 있기 때문이다.