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

Turtle·2024년 7월 4일
0
post-thumbnail

🙄경로 표현식

  • ✔️경로 표현식 → .(점)을 찍어 객체 그래프를 탐색하는 것
select m.username			// m.username : 상태 필드
  from Member as m
    join m.team t			// m.team : 단일 값 연관필드
    join m.orders o			// m.orders : 컬렉션 값 연관필드
  where t.name = '팀A';	
  • ✔️경로 표현식 용어 정리
    • 상태 필드 : 단순히 값을 저장하기 위한 필드
    • 연관 필드 : 연관관계를 위한 필드
      • 단일 값 연관필드 : @ManyToOne, @OneToOne, 대상이 엔티티
      • 컬렉션 값 연관필드 : @OneToMany, @ManyToMany, 대상이 컬렉션
  • ✔️경로 표현식 특징
    • 상태 필드 : 경로 탐색의 끝, 탐색X
    • 단일 값 연관경로 : 묵시적 내부 조인 발생, 탐색O
    • 컬렉션 값 연관경로 : 묵시적 내부 조인 발생, 탐색X
      • FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능

✔️경로 표현식(상태 필드)

String query = "select m.name from Member as m";	// 상태 필드
List<String> resultList = em.createQuery(query, String.class).getResultList();

for (String s : resultList) {
	System.out.println("s = " + s);
}

경로 표현식(상태 필드) 실행 결과

Hibernate: 
    /* select
        m.name 
    from
        Member as m */ select
            m1_0.name 
        from
            MEMBERS m1_0

✔️경로 표현식(단일 값 연관필드) → 묵시적 내부 조인이 발생, 탐색O

String query = "select m.team from Member as m";	// 단일 값 연관필드
List<String> resultList = em.createQuery(query, String.class).getResultList();

for (String s : resultList) {
	System.out.println("s = " + s);
}

경로 표현식(단일 값 연관필드) 실행 결과

Hibernate: 
    /* select
        m.team 
    from
        Member as m */ select
            t1_0.TEAM_ID,
            t1_0.name 
        from
            MEMBERS m1_0 
        join
            Team t1_0 
                on t1_0.TEAM_ID=m1_0.TEAM_ID

✔️경로 표현식(컬렉션 값 연관필드) → 묵시적 내부 조인 발생, 탐색X

String query = "select t.members from Team as t";
List resultList = em.createQuery(query, List.class).getResultList();

for (Object object : resultList) {
	System.out.println("object = " + object);
}

경로 표현식(컬렉션 값 연관필드) 실행 결과

Hibernate: 
    /* select
        t.members 
    from
        Team as t */ select
            m1_0.MEMBER_ID,
            m1_0.age,
            m1_0.memberType,
            m1_0.name,
            t1_0.TEAM_ID,
            t1_0.name 
        from
            Team t1_0 
        join
            MEMBERS m1_0 
                on t1_0.TEAM_ID=m1_0.TEAM_ID
  • ✔️명시적 조인과 묵시적 조인
    • 명시적 조인 : join 키워드 직접 사용
      • select m from Member as m join m.Team as t
    • 묵시적 조인 : 경로 표현식에 의해 묵시적으로 SQL 조인 발생
      • select m.team from Member as m

🙄페치 조인(fetch join) - 기본

  • SQL 조인 종류가 아님
  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • join fetch 명령어 사용
  • 패치 조인
  • ✔️코드 분석
    • ①. 팀A를 만들고 이 팀에 멤버A를 넣은 것
    • ②. 팀B를 만들고 이 팀에 멤버B, 멤버C를 넣은 것
      • ① & ②번 과정을 거친 후 영속성 컨텍스트에서 관리되는 상태가 됨
    • ③-1. 회원 엔티티를 대상으로 조회하는 SELECT 쿼리 1회 호출
    • ③-2. LAZY(지연로딩)로 설정했기 때문에 SQL 쿼리문을 보면 Member 엔티티와 관련된 것들만 나와있는 상황으로 프록시로 설정되어있는 것을 볼 수 있음
    • ③-3. 실제 팀 엔티티 메서드를 호출하는 순간 데이터베이스에 쿼리가 날아가게 되므로 팀A에 대한 쿼리문 한 번 실행됨
    • ③-4. 팀B에 대한 쿼리문 한 번 실행됨
    • 결론) N+1 문제가 발생하게 됨
public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        try {
        	/////////////////////////////// ①
            Team teamA = new Team();
            teamA.setName("teamA");
            em.persist(teamA);

            Member memberA = new Member();
            memberA.setName("memberA");
            memberA.addMember(teamA);
            em.persist(memberA);
			///////////////////////////////

			/////////////////////////////// ②
            Team teamB = new Team();
            teamB.setName("teamB");
            em.persist(teamB);

            Member memberB = new Member();
            memberB.setName("memberB");
            memberB.addMember(teamB);
            em.persist(memberB);

            Member memberC = new Member();
            memberC.setName("memberC");
            memberC.addMember(teamB);
            em.persist(memberC);
            ///////////////////////////////

            em.flush();
            em.clear();

			////////////////////////////////////////////////////////////// ③
            List<Member> list = em.createQuery("select m from Member as m", Member.class).getResultList();
            for (Member member : list) {
                System.out.println("member = " + member.getName() + ", member.team = " + member.getTeam().getName());
            }
			//////////////////////////////////////////////////////////////

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

실행 결과

Hibernate: 
    /* select
        m 
    from
        Member as m */ select
            m1_0.MEMBER_ID,
            m1_0.age,
            m1_0.memberType,
            m1_0.name,
            m1_0.TEAM_ID 
        from
            MEMBERS m1_0
Hibernate: 
    select
        t1_0.TEAM_ID,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.TEAM_ID=?
member = memberA, member.team = teamA
Hibernate: 
    select
        t1_0.TEAM_ID,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.TEAM_ID=?
member = memberB, member.team = teamB
member = memberC, member.team = teamB
  • ✔️페치 조인(연관필드 페치 조인) 코드 분석
    • 회원과 팀을 지연 로딩으로 설정
    • 회원을 조회할 때 페치 조인을 사용해서 팀도 함께 조회했으므로 여기서 연관된 팀 엔티티는 프록시가 아니고 실제 엔티티다.
    • 그리고 프록시가 아닌 실제 엔티티이므로 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.
public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        try {
            Team teamA = new Team();
            teamA.setName("teamA");
            em.persist(teamA);

            Team teamB = new Team();
            teamB.setName("teamB");
            em.persist(teamB);

            Member memberA = new Member();
            memberA.setName("memberA");
            memberA.addMember(teamA);
            em.persist(memberA);

            Member memberB = new Member();
            memberB.setName("memberB");
            memberB.addMember(teamB);
            em.persist(memberB);

            Member memberC = new Member();
            memberC.setName("memberC");
            memberC.addMember(teamB);
            em.persist(memberC);

            em.flush();
            em.clear();
			
            //////////////////////////////////////////////////////////////
            List<Member> list = em.createQuery("select m from Member as m join fetch m.team as t", Member.class).getResultList();
            for (Member member : list) {
                System.out.println("member = " + member.getName() + ", member.team = " + member.getTeam().getName());
            }
            //////////////////////////////////////////////////////////////

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

실행 결과

Hibernate: 
    /* select
        m 
    from
        Member as m 
    join
        
    fetch
        m.team as t */ select
            m1_0.MEMBER_ID,
            m1_0.age,
            m1_0.memberType,
            m1_0.name,
            t1_0.TEAM_ID,
            t1_0.name 
        from
            MEMBERS m1_0 
        join
            Team t1_0 
                on t1_0.TEAM_ID=m1_0.TEAM_ID
member = memberA, member.team = teamA
member = memberB, member.team = teamB
member = memberC, member.team = teamB
  • ✔️페치 조인(컬렉션 페치 조인) 코드 분석
    • SQL의 DISTINCT는 중복된 결과를 제거하는 명령이다.
    • JPQL의 DISTINCT는 SQL에 DISTINCT를 추가하는 것은 물론이고 애플리케이셚에서 한 번 더 중복을 제거한다.
public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        try {
            Team teamA = new Team();
            teamA.setName("teamA");
            em.persist(teamA);

            Team teamB = new Team();
            teamB.setName("teamB");
            em.persist(teamB);

            Member memberA = new Member();
            memberA.setName("memberA");
            memberA.addMember(teamA);
            em.persist(memberA);

            Member memberB = new Member();
            memberB.setName("memberB");
            memberB.addMember(teamB);
            em.persist(memberB);

            Member memberC = new Member();
            memberC.setName("memberC");
            memberC.addMember(teamB);
            em.persist(memberC);

            em.flush();
            em.clear();

			//////////////////////////////////////////////////////////////
            List<Team> list = em.createQuery("select distinct t from Team as t join fetch t.members", Team.class).getResultList();
            for (Team team : list) {
                System.out.println("team.name = " + team.getName() + ", team.size = " + team.getMembers().size());
            }
            //////////////////////////////////////////////////////////////

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

실행 결과

Hibernate: 
    /* select
        distinct t 
    from
        Team as t 
    join
        
    fetch
        t.members */ select
            distinct t1_0.TEAM_ID,
            m1_0.TEAM_ID,
            m1_0.MEMBER_ID,
            m1_0.age,
            m1_0.memberType,
            m1_0.name,
            t1_0.name 
        from
            Team t1_0 
        join
            MEMBERS m1_0 
                on t1_0.TEAM_ID=m1_0.TEAM_ID
team.name = teamA, team.size = 1
team.name = teamB, team.size = 2
  • ✔️정리
    • JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다.
    • 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
    • 반면에 페치 조인을 사용하면 연관된 엔티티도 함께 조회한다.
    • 페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어 SQL 호출 횟수를 줄여 성능을 최적화(Ex. N+1 문제 해결)할 수 있다.
    • 엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라고 불린다.
    • 페치 조인은 글로벌 로딩 전략보다 우선한다. 예를 들어 글로벌 로딩 전략을 지연 로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페이 조인을 적용해서 함께 조회한다.
    • 최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에서 항상 즉시 로딩이 일어난다. 물론 일부는 빠를 수 있지만 전체로 보면 사용하지 않는 엔티티를 자주 로딩하므로 오히려 악영향을 미칠 수 있다. 따라서 글로벌 전략은 되도록 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다.
    • 또한 페치 조인을 사용하면 연관된 엔티티를 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않는다. 따라서 준영속 상태에서도 객체 그래프 탐색이 가능하다.

🙄페치 조인(fetch join) - 한계

  • ❗페치 조인 대상에는 별칭을 줄 수 없다.
    • JPA 표준에서는 지원하지 않지만 하이버네이트를 포함한 몇몇 구현체들은 페치 조인에 별칭을 지원한다. 하지만 별칭을 잘못 사용하면 연관된 데이터 수가 달라져서 데이터 무결성이 깨질 수 있으므로 조심해서 사용해야 한다.
  • ❗둘 이상의 컬렉션은 페치할 수 없다.
    • 구현체에 따라 되기도 하는데 컬렉션 x 컬렉션의 카티시안 곱이 만들어지므로 주의해야 한다.
  • ❗컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
    • 컬렉션이 아닌 단일 값 연관필드들은 페치 조인을 사용해도 페이징 API를 사용할 수 있다.
    • 하이버네이트에서 컬렉션을 페치 조인하고 페이징 API를 사용하면 경고 로그를 남기면서 메모리에서 페이징 처리를 한다. 데이터가 적으면 문제없지만 데이터가 많으면 성능 이슈와 메모리 초과 예외가 발생할 수 있어 위험하다.

🙄다형성 쿼리

  • ✔️다형성 쿼리
    • JPQL로 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회가 된다.
    • TYPE은 엔티티 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용한다.
    • TREAT(JPA 2.1) : 자바의 타입 캐스팅과 유사하며 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.
public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        try {
            Book book1 = new Book();
            Book book2 = new Book();
            Album album = new Album();
            em.persist(book1);
            em.persist(book2);
            em.persist(album);

            em.flush();
            em.clear();

			//////////////////////////////////////////////////////////////
            List<Item> resultList = em.createQuery("select i from Item as i where type(i) in (Book)", Item.class).getResultList();
            for (Item item : resultList) {
                System.out.println("item = " + item);
            }
            //////////////////////////////////////////////////////////////

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

실행 결과

Hibernate: 
    /* select
        i 
    from
        Item as i 
    where
        type(i) in (Book) */ select
            i1_0.ITEM_ID,
            i1_0.DTYPE,
            i1_0.name,
            i1_0.singer,
            i1_0.author,
            i1_0.isbn 
        from
            Item i1_0 
        where
            i1_0.DTYPE in ('B')
item = hellojpa.domain.Book@342a1f84
item = hellojpa.domain.Book@72001c71

🙄엔티티 직접 사용 - 기본키 값, 외래키 값

  • ✔️엔티티 직접 사용 - 기본키 값(PK)
    • 객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본키 값으로 식별한다.
    • 따라서 JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본키 값을 사용한다.

🙄Named 쿼리

  • ✔️Named 쿼리
    • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
    • 정적 쿼리
    • 어노테이션, XML에 정의
    • 애플리케이션 로딩 시점에 초기화 후 재사용
    • 애플리케이션 로딩 시점에 쿼리를 검증

🙄벌크 연산

  • ✔️벌크 연산
    • 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
    • UPDATE, DELETE 지원
    • INSERT(insert into .. select, 하이버네이트 지원)
    • 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리
    • 벌크 연산을 먼저 실행
    • 벌크 연산 수행 후 영속성 컨텍스트 초기화

0개의 댓글