GitHub

https://github.com/Choidongjun0830

Spring

[JPA 기본편] 객체지향 쿼리 언어2 - 중급 문법

gogi masidda 2024. 6. 26. 17:07

경로 표현식

  • .(점)을 찍어 객체 그래프를 탐색하는 것
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