경로 표현식
경로 표현식: .(점)을 찍어 객체 그래프를 탐색하는 것
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"; //명시적 조인
-------------------------------
String query = "select t.members.username from Team t ";
// 묵시적 조인 상태에서 members가 컬랙션인 경우 이 쿼리는 실패한다. 탐색이 불가능하다.
-------------------------------
String query = "select m.username from Team t join t.members m";
- 묵시적 조인이 아닌 명시적 조인을 이용해라.
- 묵시적 조인은 조인이 일어나는 상황을 파악하기 어려우며, 외부 조인을 사용할 수 없다.
fetch join
쿼리가 두번 나갈 것 같은데 한번에 처리할 수 있게 만들어준다.
다대일 관계에서의 fetch join
- @ManyToOne 사용예시
Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setAge(10);
member1.setType(MemberType.ADMIN);
member1.changeTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setAge(10);
member2.setType(MemberType.ADMIN);
member2.changeTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setAge(10);
member3.setType(MemberType.ADMIN);
member3.changeTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "select t from Team t join fetch t.members";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
for (Member member : result) {
System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
//회원1, 팀A (SQL 호출)
//회원2, 팀A (1차 캐시)
//회원3, 팀B (SQL 호출)
//회원 100명 -> N(첫 쿼리로 받아온 데이터의 갯수) + 1(Member를 가져온 첫 쿼리)
}
루프로 돌때 proxy가 아닌 진짜 데이터이다.
일대다 관계, 컬랙션 페치 조인 (join으로 인핱 데이터 뻥튀기 주의!)
- 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 query = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
for (Team team : result) {
// 페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
System.out.println("team = " + team.getName() + " | " + team.getMembers().size());
for (Member member : team.getMembers()) {
System.out.println("- members = " + member);
}
}
- 결과
- 왜 TeamA에 해당하는 쿼리가 2번 나타나게 되는걸까?
- 멤버에 대한 페치조인으로 지연로딩 없이 즉시 Member와 join된다.
- SQL은
teamA
가member1
및member2
와 일대일로 매칭되는 두 개의 행으로 반환하게 된다. - JPA는 각 행을 독립적으로 받아들이기 때문에
teamA
객체를 두 번 생성하여 각각의member
와 연결하게 된다.
중복을 원하지 않는다면 DISTINCT를 사용하자.
DISTINCT
- jpql
select distinct t
from Team t join fetch t.members
where t.name = ‘팀A’
- DISTINCT의 기능
- sql에서의 중복을 제거한다.
- JPA에서 가져올 경우 엔티티의 중복을 제거한다.
- 결과 - 중복이 사라졌음을 알 수 있다.
페치 조인의 특징과 한계
페치 조인 대상에는 별칭을 줄 수 없다.
members
에 별칭을 주고 where
절로 조건을 주게 되면 다음과 같은 상황이 발생할 수 있다.
- (회원 1,회원 2, 회원 3)이 있다고 가정한다.
Team
의 모든Member
가 아닌 특정 조건(username = '회원1'
)에 맞는Member
만을 조회하므로, 영속성 컨텍스트에는Team
엔티티가회원1
만을 포함한 상태로 저장된다.- 이후 영속성 컨텍스트에
Team
엔티티가 이미 존재한다고 판단하여 다른Member
엔티티(회원2
,회원3
등)는 추가로 조회되지 않게 된다. - 이로 인해
Team
엔티티의members
컬렉션은 원래의 일관된 상태가 아니라 특정 조건에 맞는Member
만을 포함한 불완전한 상태가 된다.
이 문제로 인해 영속성 컨텍스트에 불일치 데이터가 생기며, Team
의 members
컬렉션이 왜곡될 수 있다.
반면, 일반 jpql은 지연로딩을 사용하여 특정 조건과 참조 값만 조회하기 때문에 영속성 컨텍스트에는 로드되지않아 정합성을 해치지않는다.
둘 이상의 컬렉션은 페치 조인이 불가능하다.
더 많은 데이터를 조회할 경우 더많은 중복데이터 생성(데이터 뻥튀기)으로 관리가 어려워 질 수 있다.
컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
→ 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
페치 조인 한계 정리
- 모든 것을 페치 조인으로 해결할 수는 없음
- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적일 것.
페치 조인 정리
- 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함 @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
- 실무에서 글로벌 로딩 전략은 모두 지연 로딩
- 최적화가 필요한 곳은 페치 조인 적용
Named Query
@Entity@NamedQueries({
@NamedQuery(
name = "Member.findMemberByName",
query = "select m from Member m where m.name = :username"),
@NamedQuery(
name = "Member.findMemberById",
query = "select m from Member m where m.id = :memberId")
})publicclassMemberextendsDateMarkable{
- 애플리케이션 로딩시점에 NameQuery를 JPA가 검증한다. 검증되지 않으면 컴파일 에러를 발생시킨다.
- spring data JPA를 사용하면 repositiory에 @Query 어노테이션을 사용하면 위과 같은 검증을 자동으로 실행해주고 해당 jpql도 사용할 수 있게해준다.
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
@Query("select m from Member m where m.name = :username")
List<Member> findByUsername(@Param("username") String username);
벌크연산
- 벌크 연산 : 쿼리 한 번에 여러 테이블 로우를 변경한다.
- 예시
- 재고가 10개 미만인 상품을 리스트로 조회시킨다.
- 상품 엔티티의 가격을 10% 증가시킨다.
- 트랜잭션 커밋 시점에 변경감지가 동작한다.
- 그렇다면 변경된 데이터가 100건이라면 100번의 UPDATE SQL이 실행될 것이다.
- → JPA의 변경감지만 활용한다면 너무 많은 쿼리가 발생할 수 있다.
- 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다.
- 따라서
- 벌크 연산을 먼저 실행하거나
- 또는 벌크 연산 수행 후 영속성 컨텍스트를 초기화 시키자.
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();
em.clear
spring data jpa에서는 @Modifying 어노테이션을 사용하여 벌크 연산을 한다.
- @Modifying 애노테이션은 기본적으로@Transactional과 함께 사용된다.
변경 작업은 트랜잭션 내에서 실행되어야 하며,완료되지 않은 변경 작업이 여러 작업에 영향을 줄 수 있기 때문이다.이를 통해 데이터베이스에 대한 변경 작업을 수행할 때 원자성(Atomicity), 일관성(Consistency), 독립성(Isolation), 지속성(Durability)을 보장할 수 있게 된다.
참고
https://data-make.tistory.com/617#google_vignette
https://hstory0208.tistory.com/entry/JPA-Modifying이란-그리고-주의할점-벌크-연산