2210.03_TIL : 자바 ORM 표준 JPA 프로그래밍 - 기본편

백승한·2022년 10월 3일
0

스프링

목록 보기
14/14

페치 조인 - 기본

  • SQL 조인 종류 X
  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • join fetch 명령어 사용
  • 페치 조인 ::= ( LEFT ( OUTER ) | INNER ) JOIN FETCH 조인경로
  • Join이랑 똑같은데 FETCH만 넣어주면된다.

엔티티 페치 조인

회원을 조회하면서 연관된 팀도 함께 조회 (SQL 한 번에)

  • 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

inner join이기때문에 회원4는 조회되지 않는다.

String query = "select m from Member m";

List<Member> resultList = em.createQuery(query, Member.class).getResultList();

for (Member member : resultList) {
	System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
    // 멤버1, teamA(SQL) - 영속성 컨텍스트에 없기때문에
    // 멤버2, teamA(1차캐시)
    // 멤버3, teamB(SQL)
}

LAZY 타입으로 설정해두었기 때문에 Team은 프록시로 올려두고 getTeam().getName()를 사용할때 DB에서 가져고오고 1차캐시에 올려두게 되어있다.

그래서 멤버1의 Team 객체를 가져올때만 select 쿼리가 날라간거고, 1차캐시에 올라갔기때문에 멤버2의 Team 객체를 가져올때는 DB가 아니라 1차캐시에서 가져온거다.

멤버3의 Team객체를 얻으려면 1차캐시에 없기때문에 SQL문에 또 나간다
-> N+1문제 발생 ( 해결방법은 join fetch )

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

  • 페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X
    이미 Member랑 Team의 데이터를 Join을 통해 다 가져왔기때문에 프록시가 아니고, 1차캐시에 다 담아져있어서 team의 데이터를 사용할때 SQL문이 날라가지않는다.

컬렉션 페치 조인

일대다 관계, 컬렉션 페치 조인

  • 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> resultList = em.createQuery(query, Team.class).getResultList();

for (Team team : resultList) {
    System.out.println(team.getName() + ", " + team.getMembers().size());
    for (Member member : team.getMembers()) {
    System.out.println("-> member = " + member);
    }
}

team 입장에서는 같은 teamA 하나인데, member 입장에서는 2개이기때문에 row가 두줄이 나온다. 따라서 같은게 2개나온다. JPA는 row가 3인지 100인지는 모른다. 그냥 결과를 보여줄 뿐인다.

페치 조인과 DISTINCT

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령
  • JPQL의 DISTINCT 2가지 기능 제공
    • SQL에 DISTINCT를 추가
    • 애플리케이션에서 엔티티 중복 제거
  • select distinct t
    from Team t join fetch t.members
    where t.name = ‘팀A’
  • SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과
    에서 중복제거 실패
  • DISTINCT가 추가로 애플리케이션에서 중복 제거시도
  • 같은 식별자를 가진 Team 엔티티 제거

    DISTINCT 추가시 결과
    teamname = 팀A, team = Team@0x100
    -> username = 회원1, member = Member@0x200
    -> username = 회원2, member = Member@0x300

페치 조인과 일반 조인의 차이

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


  • 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음, DB에서 team만 퍼올린다.
  • 단지 SELECT 절에 지정한 엔티티만 조회할 뿐
  • 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회X

페치 조인 - 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.

    • 하이버네이트는 가능, 가급적 사용X
  • !!! 둘 이상의 컬렉션은 페치 조인 할 수 없다 !!!

  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.

    • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
  • 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화

  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함

    • @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩

  • 최적화가 필요한 곳은 페치 조인 적용

페치 조인 - 정리

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

다형성 쿼리

TYPE

  • 조회 대상을 특정 자식으로 한정
    • 예) Item 중에 Book, Movie를 조회해라
  • JPQL
    • select i from Item i
      where type(i) IN (Book, Movie)
  • SQL
    • select i from i
      where i.DTYPE in (‘B’, ‘M’)

TREAT

  • 자바의 타입 캐스팅과 유사
  • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
  • FROM, WHERE, SELECT(하이버네이트 지원) 사용
  • JPQL
    • select i from Item i
      where treat(i as Book).auther = ‘kim’
  • SQL
    • select i.* from Item i
      where i.DTYPE = ‘B’ and i.auther = ‘kim’

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용
**외래키로 사용될 때도 마찬가지이다.

Named 쿼리

Member 객체

< JPA >
@NamedQuery(name = "Member.findByUsername",
            query = "select m from Member m where m.username = :username"
)            
< Spring Data JPA에서 같은 역할>
@Querty("select ***...")
Member singleResult = em.createNamedQuery("Member.findByUsername", Member.class)
                    .setParameter("username", "멤버1")
                    .getSingleResult();
System.out.println("singleResult = " + singleResult);
// singleResult = Member{id=3, username='멤버1', age=29}

벌크 연산

벌크 연산 예제

  • 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
  • executeUpdate()의 결과는 영향받은 엔티티 수 반환
  • UPDATE, DELETE 지원
  • INSERT(insert into .. select, 하이버네이트 지원)
String sqlString = "update Product p " +
                   "set p.price = p.price * 1.1 " +
                   "where p.stockAmount < :stockAmount";
                   
int resultCount = em.createQuery(sqlString)
                   .setParameter("stockAmount", 10)
                   .executeUpdate(); 

벌크 연산 주의

  • 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접
    쿼리

    • 벌크 연산을 먼저 실행

    • 벌크 연산 수행 후 영속성 컨텍스트 초기화

      // 자동 flush() 호출
      int resultCount = em.createQuery("update Member m set m.age = 20").executeUpdate();
      
      System.out.println("resultCount = " + resultCount); // 3
      
      System.out.println("member1.getAge() = " + member1.getAge()); // 29 출력
      // DB에만 반영된거기때문에 ! DB는 20살, 출력은 29살이다
      // 출력도 20살로 하기위해서는 em.clear()하고 그 다음 em.find()로 찾아와야한다.
profile
방문해주셔서 감사합니다🙂

0개의 댓글