[자바 ORM 표준 JPA 프로그래밍] 객체지향 쿼리 언어2 - 중급문법

이재표·2023년 10월 10일
0

JPQL을 통해 객체친화적으로 코드를 짤수 있다는 것을 알았다. 특히 별칭으로 참조하여 쿼리를 편리하게 짤수 있다는 큰 장점이 있었다. 이번 글에서는 JPQL의 경로표현식에 대해 자세히 알아보도록 하자!

경로 표현식

경로 표현식은 .(점)을 찍어 객체 그래프를 탐색하는 것이다.

select m.username(상태필드)
	from Member m
   	join m.team t(단일값 연관필드)
    join m.orders o(컬렉션 값 연관필드)
where t.name = '팀'

경로 표현식 필드에는 세가지 종류가 존재한다!
상태 필드 : 단순히 값을 저장하기 위한 필드(참조의 끝값)
연관필드 : 연관관계를 위한 필드(참조를 이어감)

  • 단일값 연관필드 : @ManyToOne, @OneToOne, 대상이 엔티티
  • 컬렉션 값 연관필드 : @OneToMany, @ManyToMany : 대상이 컬렉션

상태 필드
경로탐새의 끝으로, 단순 값이기 때문에 더이상 탐색이 불가하다.

String query = "select m.username from Team t join t.members m";

단일 값 연관 경로
묵시적 내부조인이 발생하며, 엔티티를 조회하므로 탐색이 가능하다. 하지만 너무 참조를 많이한다면 계속 join되기 때문에 쿼리튜닝이 어려워 진다.

String query = "select m.team from Member m";

컬렉션 값 연관 경로
묵시적 내부 조인이 발생하며, 탐색이 불가하다. 왜냐면 어떤 컬렉션으로 조회되니, 어떤 값을 참조하는지 모르기 때문에!! 그래서 명시적 조인을 통해 별칭을 얻어 별칭을 통해 탐색해야한다!!

String query = "select m.team from Member m"; //묵시적 조인
-------------------------------
String query = "select m.username from Team t join t.members m"; //명시적 조인

묵시적 조인이 아닌 명시적 조인을 이용해야한다!!

페치조인(Fetch Join)

Fetch는 SQL의 join의 한 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 단순 join의 경우 조회를 한후 연관된 엔티티를 다시 조회하기 때문에 N+1문제가 생기는 것인데, Fetch 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
두개가 같은 코드이다.

과정

그림과 같이 멤버와 팀 엔티티가 존재할때 join을 하게되면 fk를 통해 3개의 row를 가진 테이블이 만들어지게 된다. 그리고 이 결과, 5개의 엔티티를 영속성 컨텍스트에 올리는데, 객체로 표현하면 다음과 같이 fetch join의 연관관계로 나오게 된다.

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());
} 
username = 회원1, teamname = 팀A
username = 회원2, teamname = 팀A
username = 회원3, teamname = 팀B 

이때 위의 예시는 다대일의 경우였지만, 일대다의 경우 조금 문제가 생긴다.

코드
[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);
 }
}
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300 

같은 결과가 두개가 나온것을 알수 있다. 왜냐하면
일대다이기 때문에 한 팀안에 여러 멤버가 존재할수 있게된다. 이때 join을 하게되면 팀을 참조하는 로우가 두개존재하게 되므로 두개의 로우를 가진 테이블이 나오게 되기 때문에 같은 값을 가진 테이블이 2개가 나오게된다!!

이때 중복된 값을 가진 테이블이 컬렉션의 수만큼 나오기 때문에 중복을 제거하고 한개 객체만 가지고 싶다면 Distinct를 사용하면 된다.

원래 SQL에 Distinct를 사용하면 완전 데이터가 같아야하지만 jpql의 경우 같은 식별자라면 중복을 제거해주기 때문에 컬렉션 페치 조인의 중복을 제고해줄수 있다.

페치조인의 특징과 한계

  • 페치 조인 대상에는 별칭을 줄수 없다. 페치 조인의 경우 where나 on과 같이 조건을 줘서 몇몇 엔티티만 가져오는 것이 불가하다. 왜냐하면 페치조인은 연관된것을 모두 가져오는것이 원칙이기 때문에 별칭을 쓰는 순간 원칙이 무너지므로 지양해야한다.
  • 둘이상의 컬렉션은 페치조인 할수 없다. 왜냐하면 위의 일대다 관계에서 같은 테이블이 컬렉션의 크기만큼 만들어져 나왔는데, 둘 이상의 컬렉션을 사용시 컬렉션*컬렉션의 크기만큼 나오게 되기 때문에 금지한다.
  • 페이징 API를 쓸수 없다. 일대다 관계에서 같은

페이징의 경우 배치사이즈를 통해 해결이 가능하다.

정리

  • 모든 것을 페치 조인으로 해결할 수 는 없음
  • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면(통계 함수같은 어떠한 로직이 필요한 경우), 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적

다형성 쿼리

상속 구조에서 부모타입을 특정 자식 타입으로 다룰때 사용된다!

Item을 상속받는 세개의 자식타입에서 특정 타입을 조회하는 방법이 존재한다.

[JPQL]
select i from Item i where type(i) IN (Book,Movie)
[SQL]
select i from i where i.DTYPE in ('B','M')

나아가 자바의 타입 캐스팅과 유사한 쿼리도 존재한다.

[JPQL]
select i from Item i where treat(i as Book).auther = 'kim'
[SQL]
selct i.* from Item i where i.DTYPE ='B' and i.author='kim'

엔티티 직접 사용

JPQL에서 조회 대상을 엔티티로 직접 사용할수도 있는데, 이때 SQL에서 해당 엔티티의 기본 키값을 사용한다.

[JPQL]
select count(m.id) from Member m //엔티티의 아이디 사용
select count(m) from Member m //엔티티를 직접 사용
[SQL]
select count(m.id) as cnt from Member m //위의 두 jpql모두 같은 쿼리 실행

식별자뿐만 아닌 연관관계된 엔티티또한 외래키를 이용한다.

[JPQL]
select m from Member m where m.team = :team //엔티티를 직접 사용
select m from Member m where m.team.id=:teamId //외래키 사용
[SQL]
select m.* from Member m where m.team_id=? // 위의 2개 jpql모두 같은 쿼리 나감

Named쿼리

JPQL에서는 미리 쿼리를 지정해놓고 이름을 부여하여 사용하는 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쿼리를 정의하면서 가장 큰 장점은 애플리케이션 로딩 시점에 쿼리를 검증하기 때문에 빌드를 통해 오류가 있는 부분을 바로 알아차릴수 있다!

스프링 데이터 jpa에서 @Query 또한 Named쿼리이다!!

벌크 연산

update 또는 delete쿼리를 한번에 나가게 하는것을 벌크 연산이라 한다! jpa의 더티체킹으로 모든 sql을 update, delete시키면 100개의 업데이트쿼리가 있다면 쿼리가 100개 1000개면 쿼리가 1000개 나가게 된다! 즉 성능상 너무 큰 문제가 있기대문에 벌크 연산을 이용한다.
코드

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();

executeUpdate()를 통해 벌크연산이 진행되며, update, delete만 지원한다!!

주의
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 날리게 되기에 데이터 정합성에 문제가 있을수 있다. 따라서 해당 문제를 해결하기 위해 2가지 방법이 있다.
첫번째 방법) 벌크연산을 영속성 컨텍스트에 데이터를 올리기전 가장 먼저 사용하기!
두번째 방법) 벌크연산 수행후 em.clear()와 같이 영속성 컨텍스트를 초기화한후 다시 영속성 컨텍스트에 데이터 올리기!

0개의 댓글