경로 표현식
- .(점)을 찍어 객체 그래프를 탐색하는 것
select m.username -> 상태 필드
from Member m
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A';
m.team은 엔티티로 넘어가는 것. 이것을 단일 값 연관 필드라 함.
m.orders. orders는 보통 컬렉션. 컬렉션으로 가는 것을 컬렉션 값 연관 필드라 함.
- 상태 필드(state field): 단순히 값을 저장하기 위한 필드
- 연관 필드(association field):연관 관계를 위한 필드
- 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티
- 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션
경로 표현식 특징
- 상태 필드: 경로 탐색의 끝, 더이상 탐색 불가
- 단일 값 연관 경로: 묵시적 내부 조인 발생, 계속해서 탐색 가능
- 컬렉션 값 연관 경로: 묵시적 내부 조인 발생, 더이상 탐색 불가
- m.orders에 더 .(점)을 붙여서 다른 것 불가. (.size 제외)
- FROM절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
- 묵시적 조인을 쓰지 말고, 명시적 조인을 쓰자.
- 명시적 조인: join 키워드 직접 사용
- 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (ex. m.team)
예제
- 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의 중요 포인트. 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움.
페치 조인(fetch join)1 - 기본
- SQL의 조인 종류가 아니다
- JPQL에서 성능 최적화를 위해 제공하는 기능이다
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능이다
- join fetch 명령어를 사용한다.
- 페치 조인 := [LEFT [OUTER] | INNER] JOIN FETCH 조인 경로
- 회원을 조회하면서 연관된 팀도 함께 조회
- 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
Team teamA = new Team();
teamA.setName("Team A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("Team B");
em.persist(teamB);
Member member1 = new Member();
member1.setName("회원1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setName("회원2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setName("회원3");
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "select m from Member m join fetch m.team";
List<Member> resultList = em.createQuery(query, Member.class).getResultList();
for (Member member : resultList) {
System.out.println("member = " + member.getName() + ", " + member.getTeam().getName());
}
페치 조인으로 멤버와 팀을 함께 조회해서 팀에 프록시가 들어가지 않고, 실제 팀이 들어간다. 지연로딩이 되지 않는다. 그래서 쿼리를 여러번 보낼 일이 줄어든다.
String query = "select t from Team t join fetch t.members";
List<Team> resultList = em.createQuery(query, Team.class).getResultList();
for (Team team : resultList) {
System.out.println("team.getName() = " + team.getName() + ", team.getMembers() = " + team.getMembers());
}
일반 조인은 실행 시에 연관된 엔티티를 함께 조회하지 않고 페치 조인은 실행 시에 연관된 엔티티를 함께 조회한다. '
페치 조인을 사용할 때만 즉시 로딩이 된다고 생각하면 된다. 페치 조인으로 N+1문제를 해결한다.
페치 조인2 - 한계
- 페치 조인의 대상에는 별칭을 줄 수 없다.
- 하이버네이트는 가능하지만, 가급적 사용하면 안된다.
- 팀에 연관된 멤버를 조회하면 모두 다 조회가 되어야. 전체 중에 몇개만 조회하는 일은 없어야 한다.
- 둘 이상의 컬렉션은 페치 조인할 수 없다.
- 1대다는 데이터 뻥튀기가 일어날 수 있는데, 1대다에 1대다이므로 더더욱 안된다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
- 페이징 API: setFirstResult, setMaxResults
- 원래 페이징은 데이터베이스에서 해야하는데, 메모리에 전체 데이터를 가져온 후에 페이징한다. 메모리에서 페이징하기 때문에 전체 데이터가 나오지 않게 된다.
- 객체 그래프를 보면 전체 데이터가 있어야 함.
페치 조인 특징
- 연관된 엔티티들을 SQL 한번에 조회하여 성능 최적화
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선
- 실무에서 글로벌 로딩 전략은 모두 지연 로딩
- 최적화가 필요한 곳은 페치 조인을 적용하여 성능 최적화한다. (N+1문제가 발생하는 곳에 페치 조인을 적용한다.)
페치 조인은 객체 그래프를 유지할 때 사용하면 효과적.
조인을 통해 엔티티가 가진 모양이 아니라 다른 모양을 원하면 일반 조인을 사용해서 필요한 데이터들만 가져와서 DTO로 반환하는 것이 효과적이다.
다형성 쿼리
- Item 클래스를 상속받는 Album, Movie, Book이 있을 때
- 조회 대상을 특정 자식으로 한정할 수 있다.
- Item 중에 Book, Movie를 조회해라.
- JPQL
- select i from Item i where type(i) in (Book, Movie)
- SQL
- select i from Item i where i.DTYPE in ('B', 'M')
- treat로 다운 캐스팅처럼 사용할 수 있다.
엔티티 직접 사용
- JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
- select count(m.id) from Member m
- select count(m) from Member m
- 두 쿼리가 같다. 동일한 SQL이 실행된다.
- 엔티티를 파라미터로 전달해도 기본 키 값을 사용한다.
- m.team처럼 사용하면 m.team_id. 외래키 값을 사용한다.
Named 쿼리
- 미리 정의해서 사용하는 JPQL
- 쿼리에 이름을 부여
- 정적 쿼리
- 어노테이션(@NamedQuery), XML에 정의
- 애플리케이션 로딩 시점에 초기화하고 사용
- 애플리케이션 로딩 시점에 쿼리를 검증
- 대부분의 오류를 다 잡아
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.name = :username"
)
public class Member {
...
//쿼리 사용
Member singleResult = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getSingleResult();
System.out.println("singleResult.getName() = " + singleResult.getName());
...
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
위처럼도 사용가능. 이름 없는 Named 쿼리
벌크 연산
- 재고가 10개 미만인 모든 상품의 가격을 10% 상승시키기와 같은 것을 하나의 SQL로 여러 row의 데이터를 변경
int resultCount = em.createQuery("update Member m set m.age = 20")
.executeUpdate();
- executeUpdate()는 적용된 row 수를 리턴.
- update, delete 지원
- 하이버네이트는 insert와 select도 지원
- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리하기 때문에, 벌크 연산을 먼저 실행하거나 벌크 연산 수행 후에 영속성 컨텍스트를 초기화해야 한다.
- 벌크 연산 수행 후에 영속성 컨텍스트를 초기화하지 않으면 1차 캐시에서 데이터를 찾아오기 때문에 update나 insert, delete가 적용되지 않은 상태일 수 있다.
728x90
'Spring' 카테고리의 다른 글
[JPA 활용 1편 복습] 회원, 상품 도메인 개발 (0) | 2024.06.28 |
---|---|
[JPA 활용1 복습] 도메인 분석 설계 (0) | 2024.06.27 |
[JPA 기본편] 객체지향 쿼리 언어1 - 기본 문법 (0) | 2024.06.25 |
[JPA 기본편] 값 타입 (0) | 2024.06.24 |
[JPA 기본편] 프록시와 연관 관계 관리 (0) | 2024.06.23 |