Spring Framework/[인프런] Java ORM 표준 프로그래밍 - JPA

Chapter 11. 객체지향 쿼리 언어2 - 중급 문법

계란💕 2022. 9. 9. 12:41

11.1 경로 표현식

 

경로 표현식

select m.username -> 상태 필드
from Member m 
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'
  • 점을 찍어서 객체 그래프를 탐색할 수 있는 식을 경로 표현식이라고 한다.
  • 상태 필드(state field): 단순히 값을 저장하기 위한 필드, 경로 탐색의 끝이며 더이상 탐색 X (불가)
  • 연관 필드(association field): 연관 관계를 위한 필드
    1. 단일 값 연관 경로
      1. 대상이 엔티티인 경우를 말한다.   ex) @ManyToOne, @OneToOne
      2. 묵시적 내부 조인 발생(join키워드 직접 사용), 탐색 O
    2. 컬렉션 값 연관 경로
      • 대상이 컬렉션인 경우,   ex) @ManyToMany, @OneToMany
      • 묵시적 내부 조인 발생, 탐색 X
      • FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
  • 실무에서 명시적 조인을(JOIN 키워드 직접 사용) 써야 덜 헷갈리고  에러가 발생하지 않는다.

 

 

  Ex) 단일 값 연관 경로

<hide/>
package jpql;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Member member1 = new Member();
            member1.setUsername("관리자1");
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("관리자2");
            em.persist(member2);
            em.flush();
            em.clear();

            String query = "SELECT m.team FROM Member m";
            List<Team> result = em.createQuery(query, Team.class).getResultList();
            for (Team s : result) {
                System.out.println("s = " + s);
            }
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

  • 묵시적 내부 조인이 발생
  • 실무에서 이렇게 발생하도록 짜지 않는다.
<hide/>
 /* SELECT
        m.team 
    FROM
        Member m */ select
            team1_.id as id1_3_,
            team1_.name as name2_3_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.id

 

 

  Ex) 컬렉션 값 연관 경로

<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Member member1 = new Member();
            member1.setUsername("관리자1");
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("관리자2");
            em.persist(member2);
            em.flush();
            em.clear();

            String query = "SELECT t.members FROM Team t";
            Collection result = em.createQuery(query, Collection.class).getResultList();
            for (Object o : result) {
                System.out.println("o = " + o);
            }
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

  • 값이 없어서 출력은 안 된다.
  • t.members 뒤에 점을 찍으면 뭔가 추천되는 값이 없다. 컬렉션 자체를 가리키기 때문이다.
<hide/>
  /* SELECT
        t.members 
    FROM
        Team t */ select
            members1_.id as id1_0_,
            members1_.age as age2_0_,
            members1_.TEAM_ID as TEAM_ID5_0_,
            members1_.type as type3_0_,
            members1_.username as username4_0_ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID

 

 

  Ex)  상태 필드 - inner join 없음

<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Team team = new Team();
            em.persist(team);
            Member member1 = new Member();
            member1.setUsername("관리자1");
            member1.setTeam(team);
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("관리자2");
            member2.setTeam(team);
            em.persist(member2);
            em.flush();
            em.clear();

            String query = "SELECT t.members.size FROM Team t";
            Integer result = em.createQuery(query, Integer.class).getSingleResult();
            System.out.println("res = " + result);
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

<hide/>
 /* SELECT
        t.members.size 
    FROM
        Team t */ select
            (select
                count(members1_.TEAM_ID) 
            from
                Member members1_ 
            where
                team0_.id=members1_.TEAM_ID) as col_0_0_ 
        from
            Team team0_
res = 2

 

 

  Ex) 컬렉션 값 연관 경로

  • 그런데  t.members 뒤에는 더이상 탐색이 안된다.
  • 따라서, 명시적 조인을 써야한다. 별칭을 설정하면 별칭을 통해 탐색 가능하기 때문이다.
    • 권장: SELECT m FROM Team t JOIN t.members m
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Team team = new Team();
            em.persist(team);
            Member member1 = new Member();
            member1.setUsername("관리자1");
            member1.setTeam(team);
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("관리자2");
            member2.setTeam(team);
            em.persist(member2);
            em.flush();
            em.clear();

            String query = "SELECT t.members FROM Team t";
            Collection result = em.createQuery(query, Collection.class).getResultList();
            System.out.println("res = " + result);
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

<hide/>
    /* SELECT
        t.members 
    FROM
        Team t */ select
            members1_.id as id1_0_,
            members1_.age as age2_0_,
            members1_.TEAM_ID as TEAM_ID5_0_,
            members1_.type as type3_0_,
            members1_.username as username4_0_ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
res = [Member{id=2, username='관리자1', age=0}, Member{id=3, username='관리자2', age=0}]

 

 

명시작 조인, 묵시적 조인

  • 명시적 조인: "JOIN" 키워드를 직접 사용 
  • 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생한다. (내부 조인만 가능)
    • 외부 조인하려면 명시적 조인을 해야한다.

 

 

  Ex) 경로 표현식

  • 컬렉션은 자기 자신 또는 size() 까지만 가져올 수 있고 더 이상 탐색은 불가능하다.
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 튜닝에 중요한 포인트

 

 

 

11.2 페치 조인(fetch join) (1) - 기본

 

fetch join이란?

  • 지연로딩으로 세팅해도 즉시 로딩(EAGER)로 가져오는 쿼리이다. (지연 로딩보다 페치 조인이 우선) 
  • 페치 조인은 SQL 조인의 종류가 아니다.
  • JPQL에서 성능 최적화를 위해 제공한다.
  • 연관된 엔티티나 컬렉션을 SQL로 한번에 조회하는 기능이다.

 

 

엔티티 페치 조인

  • 회원을 조회하면서 연관된 팀도 함께 조회한다. SQL 한방 쿼리
  • SQL을 보면 회원 뿐만 아니라 팀(T.*) 도 함께 SELECT

 

  Ex) 엔티티 페치 조인 - JPQL에 따라 실제 실행되는 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

 

 

FK => PK&nbsp; (MEMBER.TEAM_ID -> TEAM.ID)

 

 

 

결과

 

 

  Ex) 페치 조인이 나오게 된 이유

<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Team teamA = new Team();
            teamA.setName("팀A");
            em.persist(teamA);
            Team teamB = new Team();
            teamB.setName("팀B");
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("회원1");
            member1.setTeam(teamA);
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamA);
            em.persist(member2);
            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            em.persist(member3);


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

            String query = "SELECT m FROM Member m";
            List<Member> result = em.createQuery(query, Member.class).getResultList();
            for (Member member : result) {
                System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
            }

            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

  • LAZY 설정되어 있어서 프록시로 들어오고 getName() 호출한 시점에야 데이터베이스에 쿼리를 날린다.
  • 쿼리가 너무 많이 나온다. "N + 1" 문제
    • LAZY, EAGER 둘다  "N + 1" 문제가 발생한다
    • fetch join으로 해결 가능
<hide/>
    /* SELECT
        m 
    FROM
        Member m */ select
            member0_.id as id1_0_,
            member0_.age as age2_0_,
            member0_.TEAM_ID as TEAM_ID5_0_,
            member0_.type as type3_0_,
            member0_.username as username4_0_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.id as id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
member = 회원1, 팀A
member = 회원2, 팀A
Hibernate: 
    select
        team0_.id as id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
member = 회원3, 팀B

 

 

  Ex) fetch join - ManyToOne

<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Team teamA = new Team();
            teamA.setName("팀A");
            em.persist(teamA);
            Team teamB = new Team();
            teamB.setName("팀B");
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("회원1");
            member1.setTeam(teamA);
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamA);
            em.persist(member2);
            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            em.persist(member3);
            em.flush();
            em.clear();

            String query = "SELECT m FROM Member m join fetch m.team";
            List<Member> result = em.createQuery(query, Member.class).getResultList();

            for (Member member : result) {
                System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
            }

            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

  • join으로 한방 쿼리가 나간다. 멤버와 팀 inner join
  • for문에서 getTeam()으로 얻는 team은 프록시가 아니다.
    • createQuery() 실행되는 시점에 얻은 팀은 fetch join으로 얻은 실제 엔티티(프록시 아님)
    • 지연로딩 없이 깔끔하게 나온다.
<hide/>
Hibernate: 
    /* SELECT
        m 
    FROM
        Member m 
    join
        fetch m.team */ select
            member0_.id as id1_0_0_,
            team1_.id as id1_3_1_,
            member0_.age as age2_0_0_,
            member0_.TEAM_ID as TEAM_ID5_0_0_,
            member0_.type as type3_0_0_,
            member0_.username as username4_0_0_,
            team1_.name as name2_3_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.id
member = 회원1, 팀A
member = 회원2, 팀A
member = 회원3, 팀B

 

 

 

컬렉션 페치 조인

  • 일대다 관계
[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'

 

 

 

  Ex)  컬렉션 페치 조인 시 주의사항

<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Team teamA = new Team();
            teamA.setName("팀A");
            em.persist(teamA);
            Team teamB = new Team();
            teamB.setName("팀B");
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("회원1");
            member1.setTeam(teamA);
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamA);
            em.persist(member2);
            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            em.persist(member3);
            em.flush();
            em.clear();

            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() + ", member: "  + team.getMembers().size() + "명");
                for(Member member : team.getMembers()){
                    System.out.println(" =>  member = " + member);
                }
            }
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

<hide/>
    /* SELECT
        t 
    FROM
        Team t 
    join
        fetch t.members */ select
            team0_.id as id1_3_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_3_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_1_,
            members1_.type as type3_0_1_,
            members1_.username as username4_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
team = 팀A, member: 2명
 =>  member = Member{id=3, username='회원1', age=0}
 =>  member = Member{id=4, username='회원2', age=0}
team = 팀A, member: 2명
 =>  member = Member{id=3, username='회원1', age=0}
 =>  member = Member{id=4, username='회원2', age=0}
team = 팀B, member: 1명
 =>  member = Member{id=5, username='회원3', age=0}
  • 팀A 내용이 왜 2개가 출력될까?
  • 데이터베이스 특성 때문에 일대다 조인은 여러 개 쿼리가 실행될 수 있다.
  • 팀, 멤버 테이블을 조인하는데 두 줄이 나온다. (멤버의 pk가 다르니까)
  • JPA는 이 시점에 개입 불가능하다. 

 

 

  Ex) FETCH JOIN이 있는 경우와 없는 경우의 result size 차이

  • JOIN FETCH 없을 때 => result size: 2
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Team teamA = new Team();
            teamA.setName("팀A");
            em.persist(teamA);
            Team teamB = new Team();
            teamB.setName("팀B");
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("회원1");
            member1.setTeam(teamA);
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamA);
            em.persist(member2);
            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            em.persist(member3);
            em.flush();
            em.clear();

            String query = "SELECT t FROM Team t";
            List<Team> result = em.createQuery(query, Team.class).getResultList();
            System.out.println("result size : " + result.size());
//            for (Team team : result) {
//                System.out.println("team = " + team.getName() + ", member: "  + team.getMembers().size() + "명");
//                for(Member member : team.getMembers()){
//                    System.out.println(" =>  member = " + member);
//                }
//            }
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

 

  • JOIN FETCH 있을 때 => result size = 3
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Team teamA = new Team();
            teamA.setName("팀A");
            em.persist(teamA);
            Team teamB = new Team();
            teamB.setName("팀B");
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("회원1");
            member1.setTeam(teamA);
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamA);
            em.persist(member2);
            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            em.persist(member3);
            em.flush();
            em.clear();

            String query = "SELECT t FROM Team t join fetch t.members";
            List<Team> result = em.createQuery(query, Team.class).getResultList();
            System.out.println("result size : " + result.size());
//            for (Team team : result) {
//                System.out.println("team = " + team.getName() + ", member: "  + team.getMembers().size() + "명");
//                for(Member member : team.getMembers()){
//                    System.out.println(" =>  member = " + member);
//                }
//            }
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

  • 위 결과가 왜 다를까?
  • 조인하면서 데이터가 커지기 때문이다.
  • 일대다 조인하면 데이터가 더 커질 수 있다는 특성 때문에 이런 현상이 생긴다.

 

 

페치 조인과 DISTINCT

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령이다.
  • JPQL의 DISTINCT는 2가지 기능 제공
    1. SQL에 DISTINCT 추가
    2. 애플리케이션에서 엔티티의 중복을 제거한다.
  • DISTINCT가 추가로 애플리케이션에서 중복 제거를 시도한다.
  • 같은 식별자를 가진 Team 엔티티를 제거한다.

 

 

  Ex) DISTINCT

  • SQL에 DISTINCT를 추가하지만 데이터가 다르기 때문에 SQL 결과에서 중복 제거 실패한다.
select distinct t
from Team t join fetch t.members
where t.name = ‘팀A’

 

 

  Ex) DISTINCT

<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Team teamA = new Team();
            teamA.setName("팀A");
            em.persist(teamA);
            Team teamB = new Team();
            teamB.setName("팀B");
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("회원1");
            member1.setTeam(teamA);
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamA);
            em.persist(member2);
            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            em.persist(member3);
            em.flush();
            em.clear();

            String query = "SELECT DISTINCT t FROM Team t join fetch t.members";
            List<Team> result = em.createQuery(query, Team.class).getResultList();
            System.out.println("result size : " + result.size());
            for (Team team : result) {
                System.out.println("team = " + team.getName() + ", member: "  + team.getMembers().size() + "명");
                for(Member member : team.getMembers()){
                    System.out.println(" =>  member = " + member);
                }
            }
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

  • 컬렉션에서 중복을 제거해준다. => size = 2
<hide/>
    /* SELECT
        DISTINCT t 
    FROM
        Team t 
    join
        fetch t.members */ select
            distinct team0_.id as id1_3_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_3_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_1_,
            members1_.type as type3_0_1_,
            members1_.username as username4_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
result size : 2
team = 팀A, member: 2명
 =>  member = Member{id=3, username='회원1', age=0}
 =>  member = Member{id=4, username='회원2', age=0}
team = 팀B, member: 1명
 =>  member = Member{id=5, username='회원3', age=0}

 

 

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

  • 일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않는다. 
  • JPQL은 결과를 반환할 때, 연관 관계를 고려하지 않는다.
    • 단지, SELECT 절에 지정한 엔티티만 조회할 뿐이다.
    • 아래 예제에서 팀 엔티티만 조회하고 회원 엔티티는 조회하지 않는다.
  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회한다. (즉시 로딩)
    • 페치 조인을 사용하면 즉시 로딩이 일어난다고 보면 된다.
  • 페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념이다. 한 방 쿼리
[JPQL]
select t
from Team t join t.members m
where t.name = ‘팀A'

[SQL]
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID 
WHERE T.NAME = '팀A'

 

 

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

  • 일반 조인
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Team teamA = new Team();
            teamA.setName("팀A");
            em.persist(teamA);
            Team teamB = new Team();
            teamB.setName("팀B");
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("회원1");
            member1.setTeam(teamA);
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamA);
            em.persist(member2);
            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            em.persist(member3);
            em.flush();
            em.clear();

            String query = "SELECT t FROM Team t join t.members";
            List<Team> result = em.createQuery(query, Team.class).getResultList();
            System.out.println("result size : " + result.size());
            for (Team team : result) {
                System.out.println("team = " + team.getName() + ", member: "  + team.getMembers().size() + "명");
                for(Member member : team.getMembers()){
                    System.out.println(" =>  member = " + member);
                }
            }
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과 - 일반 조인

  • 조인했는데 SELECT절에서 팀 id랑 name만 나온다. (윗 부분 JOIN)
  • 데이터 늘어나서 3으로 출력된다.
  • for문 돌릴 때, members가  초기화되지 않는 상태이다.
  • 컬렉션이 프록시는 아니지만 데이터가 없어서 쿼리가 쭉 나온다. (맨 아래)
<hide/>
    /* SELECT
        t 
    FROM
        Team t 
    join
        t.members */ select
            team0_.id as id1_3_,
            team0_.name as name2_3_ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
result size : 3
Hibernate: 
    select
        members0_.TEAM_ID as TEAM_ID5_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.TEAM_ID as TEAM_ID5_0_1_,
        members0_.type as type3_0_1_,
        members0_.username as username4_0_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
team = 팀A, member: 2명
 =>  member = Member{id=3, username='회원1', age=0}
 =>  member = Member{id=4, username='회원2', age=0}
team = 팀A, member: 2명
 =>  member = Member{id=3, username='회원1', age=0}
 =>  member = Member{id=4, username='회원2', age=0}
Hibernate: 
    select
        members0_.TEAM_ID as TEAM_ID5_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.TEAM_ID as TEAM_ID5_0_1_,
        members0_.type as type3_0_1_,
        members0_.username as username4_0_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
team = 팀B, member: 1명
 =>  member = Member{id=5, username='회원3', age=0}

 

  • fetch join 추가
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Team teamA = new Team();
            teamA.setName("팀A");
            em.persist(teamA);
            Team teamB = new Team();
            teamB.setName("팀B");
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("회원1");
            member1.setTeam(teamA);
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamA);
            em.persist(member2);
            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            em.persist(member3);
            em.flush();
            em.clear();

            String query = "SELECT t FROM Team t join fetch t.members";
            List<Team> result = em.createQuery(query, Team.class).getResultList();
            System.out.println("result size : " + result.size());
            for (Team team : result) {
                System.out.println("team = " + team.getName() + ", member: "  + team.getMembers().size() + "명");
                for(Member member : team.getMembers()){
                    System.out.println(" =>  member = " + member);
                }
            }
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

 

  Note) 실행 결과 - fetch join

  • 일반 조인과 다르게 맨 처음 SELECT 절에서 member.id, name 뿐만 아니라 멤버에 대한 내용도 모두 가져온다.
  • 나중에 for문을 돌려서  getName(), getMember()를 출력하면 한 번에 멤버 정보를 가져온다.
<hide/>
    /* SELECT
        t 
    FROM
        Team t 
    join
        fetch t.members */ select
            team0_.id as id1_3_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_3_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_1_,
            members1_.type as type3_0_1_,
            members1_.username as username4_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
result size : 3
team = 팀A, member: 2명
 =>  member = Member{id=3, username='회원1', age=0}
 =>  member = Member{id=4, username='회원2', age=0}
team = 팀A, member: 2명
 =>  member = Member{id=3, username='회원1', age=0}
 =>  member = Member{id=4, username='회원2', age=0}
team = 팀B, member: 1명
 =>  member = Member{id=5, username='회원3', age=0}

 

 

 

11.3  페치 조인 2 - 한계

 

페치 조인 특징과 한계

  • 페치 조인 대상에는 원칙적으로 별칭을 줄 수 없다.
    • 하이버네이트는 가능
    • 관례적으로 별칭을 쓰지 않도록 한다. (페치 조인을 여러 단계로 하는 경우는 가끔 별칭을 쓰기도 한다.)
    • 가급적 사용 X
    • 페치 조인은 연관된 것들을 모두 가져오는 기능이다.
    • ex) 팀 안에 5명이 있는데 그 중 3 명만 조회하는 경우?  => 위험하다. (지워지거나 오작동 가능성) 
      • 사상이 안 맞는 "정합성(무모순성, Data Consistency)" 이슈 때문
      • 이런 경우는 처음부터 5명 조회하는 SELECT 5명 쿼리를 날려야한다.
  • 둘 이상의 컬렉션을 페치 조인할 수 없다.
    •  이 부분도 데이터 정합성 이슈가 있다.
    • ex) Team의 orders? 
    • 일대다도 데이터 뻥튀기 되는데 이 경우는 일대다"대"다 => 데이터가 예상치 못하게 뻥튀기 가능하다.
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults) 사용 불가
    • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능 (데이터 뻥튀기가 안 되서 가능)
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징 (매우 위험)
    • 페이징은 철저히 데이터베이스 중심적이다. rows를 줄이려는 기능
  • 연관된 엔티티들은 SQL 한 번으로 조회된다. => 성능 최적화
  • 페치 조인이 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한다.
    • @OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩이다.
    • 최적화 필요한 곳에만  페치 조인 적용하면 대부분 성능 문제가 해결된다.

 

페치 조인 정리

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

 

 

  Ex) 컬렉션 페치조인과 페이징 API

<hide/>
List<Team> result = em.createQuery(query, Team.class)
            .setFirstResult(0)		// 조회 시작할 위치
            .setMaxResults(1)		// 조회할 데이터 수
            .getResultList();

 

  Note) 실행 결과

  • DB에서 팀에 대한 데이터를 다 끌고 온다. 장애 나기 좋은 조건이다.
<hide/>
Hibernate: 
    /* SELECT
        t 
    FROM
        Team t 
    join
        fetch t.members */ select
            team0_.id as id1_3_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_3_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_1_,
            members1_.type as type3_0_1_,
            members1_.username as username4_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
result size : 1
team = 팀A, member: 2명
 =>  member = Member{id=3, username='회원1', age=0}
 =>  member = Member{id=4, username='회원2', age=0}
  • 경고 - 페치조인과 페이징
WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!

 

 

 

  Ex) 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능 

 

  • 기존의 쿼리
String query = "SELECT t FROM Team t join fetch t.members";

 

  • 바뀐 쿼리 - 멤버부터 시작
String query = "SELECT m FROM Member m join fetch m.team t";

 

  Note) 실행 결과 - 회원 => 팀 다대일은 페이징에 문제가 없다.

 

 

 

  Ex) BatchSize를 쓰게 된 배경

  • BatchSize는 하위 엔티티를 로딩할 때, 한번에 상위 엔티티 ID를 지정한 숫자만큼 in Query로 로딩한다.
<hide/>
 String query = "SELECT t FROM Team t";
            List<Team> result = em.createQuery(query, Team.class)
                .setFirstResult(0)
                .setMaxResults(2)
                .getResultList();

  Note) 실행 결과

  • lazy 로딩이 2번 일어난다. 성능 안 좋다.
  • 팀을 10개 조회하면 연관된 멤버를 조회하는데 추가적으로 쿼리가 10번 이상 나갈 것이다.
    • 해결 방법: @BatchSize()
<hide/>
    /* SELECT
        t 
    FROM
        Team t */ select
            team0_.id as id1_3_,
            team0_.name as name2_3_ 
        from
            Team team0_ limit ?
result size : 2

-- 여기부터 중요
Hibernate: 
    select
        members0_.TEAM_ID as TEAM_ID5_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.TEAM_ID as TEAM_ID5_0_1_,
        members0_.type as type3_0_1_,
        members0_.username as username4_0_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
team = 팀A, member: 2명
 =>  member = Member{id=3, username='회원1', age=0}
 =>  member = Member{id=4, username='회원2', age=0}
 
 
Hibernate: 
    select
        members0_.TEAM_ID as TEAM_ID5_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.TEAM_ID as TEAM_ID5_0_1_,
        members0_.type as type3_0_1_,
        members0_.username as username4_0_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
team = 팀B, member: 1명
 =>  member = Member{id=5, username='회원3', age=0}

 

 

  Ex) @BatchSize

  • 팀 클래스
<hide/>
@OneToMany(mappedBy = "team")
@BatchSize(size = 100)
private List<Member> members = new ArrayList<>();
  • Main 클래스
<hide/>
String query = "SELECT t FROM Team t";
        List<Team> result = em.createQuery(query, Team.class)
            .setFirstResult(0)
            .setMaxResults(2)
            .getResultList();

  Note) 실행 결과

  • 사이즈는 2개로 정상적으로 나온다.
  • 그런데 그 아래 쿼리를 보면?
    • 아래의 select에서 멤버를 가져오는데 in Query 안에 "?, ?"라고 나온다. TeamA, TeamB와 연관된 내용을 모두 가져온 것이다.
      • (기존에 @BatchSize를 쓰기 전에는 @  where members0_.TEAM_ID=? 이었다.)
    • 팀에서 멤버스를 가져올 때, LAZY 로딩 상태이다.
    • 매인 클래스에서 List<>에 담긴 Team을 in Query로 100 개씩 넘긴다.
    • 쿼리 하나 + lazy 로딩 쿼리 n개 => n + 1 문제
      • 페치 조인 또는 batchSize() 로 해결한다.
<hide/>
    /* SELECT
        t 
    FROM
        Team t */ select
            team0_.id as id1_3_,
            team0_.name as name2_3_ 
        from
            Team team0_ limit ?
result size : 2
Hibernate: 
    /* load one-to-many jpql.Team.members */ select
        members0_.TEAM_ID as TEAM_ID5_0_1_,
        members0_.id as id1_0_1_,
        members0_.id as id1_0_0_,
        members0_.age as age2_0_0_,
        members0_.TEAM_ID as TEAM_ID5_0_0_,
        members0_.type as type3_0_0_,
        members0_.username as username4_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?
        )
team = 팀A, member: 2명
 =>  member = Member{id=3, username='회원1', age=0}
 =>  member = Member{id=4, username='회원2', age=0}
team = 팀B, member: 1명
 =>  member = Member{id=5, username='회원3', age=0}

 

  • @BatchSize 대신에  properties에 추가하더라도 같은 기능을 사용 가능
    • 쿼리가 n + 1이 아니라 테이블 수를 맞출 수 있어서 최적화 가능하다.
<property name="hibernate.default_batch_fetch_size" value="100" />

  Note) 실행 결과 - 결과는 동일하다.

 

 

 

11.4 다형성 쿼리

 

TYPE

  • 조회 대상을 특정 자식으로 한정한다.

  Ex) Item 중에 Book(B), Movie(M)를 조회

[JPQL]
select i from Item i
where type(i) IN (Book, Movie) 

[SQL]
select i from i
where i.DTYPE in (‘B’, ‘M’)

 

 

TREAT (JPA 2.1)

  • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. (Java의 타입 캐스팅과 유사)
  • FROM, WHERE, SELECT(하이버네이트 지원) 사용

 

 

  Ex) TREAT 

  • 부모 Item, 자식 Book 
  • 다운 캐스팅 처럼 사용
[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’

 

 

 

11.5 JPQL 엔티티 직접 사용

 

엔티티 직접 사용 - 기본 키 값

  • JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
    • COUNT() 안에 멤버의 기본 키를 직접 넣어 버린다.
    • 비교하기 위한 값은 PK니까 ID가 매개변수로 들어간다.
[JPQL]
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용 

[SQL](JPQL 둘다 같은 다음 SQL 실행)
select count(m.id) as cnt from Member m

 

 

  Ex) 엔티티를 파라미터로 전달하나 식별자를 전달하나 결과는 동일

<hide/>
[엔티티를 파라미터로 전달]
String jpql = “select m from Member m where m = :member”; 
List resultList = em.createQuery(jpql) 
 .setParameter("member", member) 
 .getResultList(); 
 
[식별자를 직접 전달]
String jpql = “select m from Member m where m.id = :memberId”; 
List resultList = em.createQuery(jpql) 
 .setParameter("memberId", memberId) 
 .getResultList(); 

[실행된 SQL]
select m.* from Member m where m.id=?

 

 

엔티티 직접 사용 - 외래 키 값

 

  Ex)

  •  아래에서 where절에 m.team: 외래 키, 파라미터 team은 기본 키
<hide/>
Team team = em.find(Team.class, 1L); 

String qlString = “select m from Member m where m.team = :team”; 
List resultList = em.createQuery(qlString) 
 .setParameter("team", team) 
 .getResultList(); 

String qlString = “select m from Member m where m.team.id = :teamId”; 
List resultList = em.createQuery(qlString) 
 .setParameter("teamId", teamId) 
 .getResultList()
 
 [실행된 SQL]
 select m.* from Member m where m.team_id=?

 

 

 

11.6 Named 쿼리

 

Named 쿼리 - 정적 쿼리

  • 엔티티에 이름이 부여된 쿼리를 미리 등록해두고 사용하는 기능을 말한다. 쿼리를 재활용한다.
  • 정적 쿼리만 가능하다.
  • 애너테이션이나 XML에 Named 쿼리를 정의한다.
  • 애플리케이션 로딩 시점에 초기화 후, 재사용한다.
    • 정적 쿼리는 변하지 않는다. 따라서, 로딩 시점에 쿼리를  SQL로 파싱하고 캐시한다. 
    • JPQL는 기본적으로 SQL로 변환한 다음에 실행되는데 네임드 쿼리를 로딩 시점에 딱 한 번 실행되고 캐시되니까 비용이 거의 없다고 봐도된다.
  • 애플리케이션 로딩 시점에 쿼리를 검증한다.

 

 

  Ex) Named 쿼리 - 애너테이션

  • Member 클래스
    • 여기에 쿼리를 잘못 입력하면 SQLsyntax 에러가 난다.
    • 클래스 이름을 앞에 붙여주는 게 관례
<hide/>
@Entity
@NamedQuery(
    name = "Member.findByUsername",
    query = "SELECT m FROM Member m WHERE m.username = :username"
)
public class Member {
...
}
  • Main
    • username을 "회원1"로 수정한다.
<hide/>
List<Member > resultList =  em.createNamedQuery("Member.findByUsername", Member.class)
    .setParameter("username", "회원1")
    .getResultList();
for (Member member : resultList) {
    System.out.println("member = " + member);
}

 

  Note) 실행 결과

  • 회원1번 데이터를 가져온다.
  • 쿼리가 실행되서 이름이 바뀐 채로 출력된다.
<hide/>
    /* Member.findByUsername */ select
        member0_.id as id1_0_,
        member0_.age as age2_0_,
        member0_.TEAM_ID as TEAM_ID5_0_,
        member0_.type as type3_0_,
        member0_.username as username4_0_ 
    from
        Member member0_ 
    where
        member0_.username=?
member = Member{id=3, username='회원1', age=0}

 

 

 

Named 쿼리  - XML에 정의

  • persistence에 ormMember라는 mapping파일에 넣는다.
  • 아래와 같이  데이터를 넣어주면 네임드 쿼리를 불러올 때, 애너테이션에서 불러올 수도 있고 ormMember라는 xml 파일에서도 불러올 수 있다.
  • xml이 항상 우선권을 가진다.
  • 애플리케이션 운영 환경에 따라 다른 XML을 배포 가능
[META-INF/persistence.xml]
<persistence-unit name="jpabook" >
 <mapping-file>META-INF/ormMember.xml</mapping-file>


[META-INF/ormMember.xml]
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
     <named-query name="Member.findByUsername">
         <query><![CDATA[
             select m
             from Member m
             where m.username = :username
         ]]></query>
     </named-query>
 <named-query name="Member.count">
 <query>select count(m) from Member m</query>
 </named-query>
</entity-mappings>

 

 

cf) Named 쿼리 - JpaRepository

  • 스프링 jpa를 쓰면 JpaRepository를 상속 받은 인터페이스의 메서드 위에 @Query()를 선언하고 괄호 안에 Named 쿼리를 쓸 수 있다.
  • jpa가 이를 네임드 쿼리로 등록한다. 애플리케이션 로딩 시점에 파싱도 하고 오류도 잡아 낸다. - 큰 장점
  • 위의 예제대로 쓰는 것보다 이 방법이 더 좋다.

 

 

11.7 벌크(bulk) 연산

 

벌크 연산

  • ex) 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
  • JPA 변경 감지 기능으로 실행하려면 SQL이 너무 많이 실행된다.
    1. 재고가 10개 미만인 상품을 리스트로 조회
    2. 상품 엔티티의 가격을 10% 증가한다.
    3. 트랜잭션 커밋 시점에 변경 감지가 동작한다.
  • 변경된 데이터가 100건이라면 100번의 UPDATE SQL이 실행된다.

 

 

벌크 연산 예제

  • 쿼리 한 번으로 여러 테이블(엔티티)의 로우를 일괄적으로 변경한다. (엔티티)
  • executeUpdate(): 쿼리를 실행하고 나서 영향 받은 엔티티의 수를 반환한다.
  • UPDATE, DELETE 지원한다.
  • INSERT(하이버네이트는 insert into ... SELECT 지원)

 

 

벌크 연산 주의 사항

  • 벌크 연산은 영속성 컨텍스에 저장하는 과정을 생략하고 데이터베이스에 직접 쿼리한다.
    • 데이터 정합성에 어긋날 수 있다.
    • 아래 두 가지 중 선택해서 쓴다.
    • 벌크 연산을 먼저 실행한다.
    • 벌크 연산 수행 후, 영속성 컨텍스트를 초기화한다. em.clear().

 

 

  Ex) 벌크 연산

  • 초기화를 안 화면? 
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {

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

        try{
            Team teamA = new Team();
            teamA.setName("팀A");
            em.persist(teamA);
            Team teamB = new Team();
            teamB.setName("팀B");
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("회원1");
            member1.setTeam(teamA);
            member1.setAge(0);
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamA);
            member2.setAge(0);
            em.persist(member2);

            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            member3.setAge(0);
            em.persist(member3);
            em.flush();
            em.clear();

            // flush 되는 시점 (DB에 Age가 0으로 들어간다. 영속성 컨텍스트에서 지워지는 건 아니다.)
            int resultCnt =  em.createQuery("UPDATE Member m set  m.age = 20")  // 그런데, DB에는 20으로 업데이트
                 .executeUpdate();
            System.out.println("resultCnt = " + resultCnt);
            System.out.println("member1.getAge() = " + member1.getAge());
            System.out.println("member2.getAge() = " + member2.getAge());
            System.out.println("member3.getAge() = " + member3.getAge());
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

 

  Note) 실행 결과

  • createQuery()에서 설정한대로 나이가 20살이 들어가지 않는다.
    • 초기화 em.clear()를 안했기 때문이다.
    • 초기화하면 영속성 컨텍스트의 내용이 모두 지워져서 그 다음에 em에서 조회하는 경우, 데이터베이스의 정보를 가져온다.
<hide/>
    /* UPDATE
        Member m 
    set
        m.age = 20 */ update
            Member 
        set
            age=20
resultCnt = 3
member1.getAge() = 0
member2.getAge() = 0
member3.getAge() = 0

 

  • 올바른 벌크 연산
<hide/>
int resultCnt =  em.createQuery("UPDATE Member m set  m.age = 20")  // 그런데, DB에는 20으로 업데이트
     .executeUpdate();
em.clear();
Member findMember = em.find(Member.class, member1.getId()); // 영속성 컨텍스트에 값이 없으니까 DB에서 가져온다.
System.out.println("findMember = " + findMember.getAge());

  Note) 실행 결과 - findMember = 20

 

 

 

 

출처 - https://www.inflearn.com/course/ORM-JPA-Basic

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com