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

born_a·2022년 9월 25일
0

경로표현식

JpaMain.java

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 team : result) {
                System.out.println("team = " + team);
            }

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }

        emf.close();
    }
}

/* */ : jpql
나머지 부분은 sql 쿼리!

sql쿼리에서 멤버랑 팀을 조인해서 팀을 select projection에 나열한것을 확인할 수 있다.
실무에서는 이렇게 묵시적인 내부조인이 발생하게 쿼리를 짜면 안된다.

컬렉션은 컬렉션 그대로 가지고 오거나 사이즈만 가지고 올 수 있다.
ex) String query = "select t.members.size from Member m";

또는 컬렉션을 가지고 올 수 있다.

String query = "select t.members from Member m";
List<Collection> result = em.createQuery(query, Collection.class);

select m.username from Team t join t.members m

묵시적 조인 말고 명시적 조인을 써라!
명시적 조인을 써야 쿼리 튜닝이 쉽다.

패치조인

실무에서 정말정말 중요!!

select projection에 m만 적었는데, 실제로 실행된 sql 에서는 M의 데이터와 T의 데이터를 모두 나열한다.

패치조인 적용 전

JpaMain.java

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(teamB);
            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());
                //회원1, 팀A(SQL쿼리로 가져옴. 영속성 컨텍스트에 없기 때문)
                //회원2, 팀A(jpa한테 팀A를 달라고함, 그럼 jpa가 1차캐시에서 가져옴.그래서 쿼리 없이 바로 출력된것)
                //회원3, 팀B(1차 캐시에 없어서 쿼리를 날려서 1차 캐시에 올리고 결과를 반환)
            }

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }

        emf.close();
    }
}

N+1 문제 : 1번째 날린 쿼리로 얻은 결과만큼 n번 나옴.

문제 해결 -> 패치 조인

패치 조인 적용

이때에는 getTeam()할때 프록시가 아니라 실제 팀이 들어감.
JpaMain.java

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();

            //조인을 하긴 하는데 fetch(한번에 가져옴)
            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();
        } 


inner join으로 쿼리가 한방에 나가는 것을 확인 가능

컬렉션 패치 조인

JpaMain.java

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();

            //조인을 하긴 하는데 fetch(한번에 가져옴)
            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() + " |members = " + team.getMembers().size());
            }

            tx.commit();
        } 


근데 중복으로 출력되지?
db에서는 일대다 조인하면 데이터가 뻥튀기가 된다.

팀A입장에서는 회원1,회원2가 있다.
이걸 멤버와 조인하면 팀A입장에서는 두개의 row가 발생.
따라서 db에서 두 줄을 가져오는 것이다.

JpaMain.java

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();

            //조인을 하긴 하는데 fetch(한번에 가져옴)
            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() + " |members = " + team.getMembers().size());
                for (Member member : team.getMembers()) {
                    System.out.println(" --> member = " + member);
                }
            }

            tx.commit();
        }

중복 싫은데 어떡하지?

패치 조인과 DISTINCT

애플리케이션에서 올라올때, 이 컬렉션에 담기는 애가 중복 제거해야하는구나 하고 jpa가 걸러준다.

참고로 다대일 관계는 뻥튀기가 안된다.
일대다만 갯수가 뻥튀기됨.

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

일반조인은 select절에서 team만 조회함.
데이터가 로딩 시점에 로딩이 안되어서 select쿼리가 다 나감.

패치조인 2 - 한계

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

최적화

다시 정리할것.(13:00~19:00)

@BatchSize를 쓰게 되면 N+1이 아니라 테이블 수 만큼 맞출 수가 있다.
1.Team.java에

@BatchSize(size = 100)
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();

추가
2.persistence.xml에 <property name="hibernate.default_batch_size" value="100" /> 추가

다형성 쿼리

엔티티 직접 사용

엔티티가 db로 넘어가면 db의 pk값이다

파라미터로 엔티티 넘기기

JpaMain.java

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 where m= :member";

            Member findMember = em.createQuery(query, Member.class)
                    .setParameter("member", member1)
                    .getSingleResult();

            System.out.println("findMember = " + findMember);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }

        emf.close();
    }
}

파라미터로 pk값 넘기기

JpaMain.java

            String query = "select m from Member m where m.id= :memberId";

            Member findMember = em.createQuery(query, Member.class)
                    .setParameter("memberId", member1.getId())
                    .getSingleResult();

            System.out.println("findMember = " + findMember);

            tx.commit();

두 경우다 출력이 같다.

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

m.team = TEAM_ID이다.
m.team은 fk랑 매핑이 되어있다.

JpaMain.java

String query = "select m from Member m where m.team =:team";

            List<Member> members = em.createQuery(query, Member.class)
                    .setParameter("team", teamA)
                    .getResultList();

            for (Member member : members) {
                System.out.println("member = " + member);

Named 쿼리

쿼리에 이름을 부여할 수 있다.

애플리케이션 로딩 시점에 초기화 후 재사용
: 애플리케이션 로딩 시점에, jpa가 sql로 파싱을 해서 캐시하고 있다.

Member.java

@Entity
@NamedQuery(
        name = "Member.findByUsername", // 엔티티명. 으롶 관례상 표현
        query = "select m from Member m where m.username = :username"
)
public class Member {

JpaMain.java

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();


            List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
                    .setParameter("username", "회원1")
                    .getResultList();

            for (Member member : resultList) {
                System.out.println("member = " + member);
            }
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }

        emf.close();
    }
}

벌크 연산

JpaMain.java

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();

            int resultCount = em.createQuery("update Member m set m.age = 20")
                    .executeUpdate();

            System.out.println("resultCount = " + resultCount);
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }

        emf.close();
    }
}

벌크 연산 주의

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리가 들어간다.
잘못하면 꼬일 수 있다.

JpaMain.java

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();

            //flush 자동 호출 ; commit 될 때나, 쿼리 나가거나,강제 플러시 호출 할 때
            //flush가 되는 순간 셋다 디비에 0 0 0 으로 insert가 되고, update쿼리가 나간다..flush라는건 영속성 컨텍스트에 있는 것을 디비에 반영하는 것.
            int resultCount = em.createQuery("update Member m set m.age = 20") //db에 강제로 업데이트함. 영속성 컨텍스트에는 반영이 안되어 있음.
                    .executeUpdate();

            System.out.println("resultCount = " + resultCount);

            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();
    }
}


영속성 컨텍스트에는 반영이 안되어있지만,

db에는 정상 반영이 되어있다.

어떻게 풀까?

• 벌크연산을 먼저 실행한다.

db에 데이터가 먼저 있을때 ! 이 예제에서는 사용 안됨.

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

JpaMain.java

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();

            //flush 자동 호출 ; commit 될 때나, 쿼리 나가거나,강제 플러시 호출 할 때
            int resultCount = em.createQuery("update Member m set m.age = 20") //db에 강제로 업데이트함. 영속성 컨텍스트에는 반영이 안되어 있음.
                    .executeUpdate();

            em.clear(); // 영속성 컨텍스트에 있는 것을 지운다

            //영속성 컨텍스트에 값이 없기 때문에 디비에서 가져온다
            Member findMember = em.find(Member.class, member1.getId());

            System.out.println("findMember = " + findMember.getAge());

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }

        emf.close();
    }
}

0개의 댓글

관련 채용 정보