개요
지난 게시글(https://jaimemin.tistory.com/1997)에 이어 아래의 JPQL 개념에 대해 정리해보겠습니다.
- 경로 표현식
- Fetch Join
- 엔티티 파라미터
- Named 쿼리
- 벌크 연산
1. 경로 표현식
- 엔티티의 getter와 동일한 개념
- ex) SELECT e.id FROM Employee e
- e.id와 같이 .을 찍어 객체 그래프를 탐색하는 것을 경로 표현식이라고 함
- 경로 표현식은 3가지 종류가 존재
- 상태 필드
- 단일 값 연관 필드
- 컬렉션 값 연관 필드
1.1 상태 필드(state field)
- 단순히 값을 저장하기 위한 필드 (e.name과 같은 필드)
- 경로 탐색의 끝 즉, 이후에 더 이상 점을 찍을 수 없음
- ex) SELECT e.name, e.age FROM Employee e
1.2 연관 필드(association field)
- 연관관계를 위한 필드
- 단일 값 연관 필드
- 컬렉션 값 연관 필드
- 단일 값 연관 필드
- @ManyToOne, @OneToOne 관계에 있는 엔티티가 대상
- 경로 탐색을 이어나갈 수 있음
- ex) SELECT e.company.location FROM Employee e
- 직원이 속해있는 회사의 위치를 조회 가능 (핵심은 company에서 더 이어서 점을 찍어 탐색을 할 수 있음)
- JPQL로 작성하는 쿼리 자체는 간단하지만 묵시적 Inner Join이 발생하기 때문에 쿼리 튜닝 어려움
- JPQL: SELECT e.company.location FROM Employee e
- 실제 실행되는 SQL: SELECT c.location FROM Member m INNER JOIN Company c ON m.company_id = c.id
- 이처럼 묵시적으로 JOIN이 발생하기 때문에 쿼리 튜닝하기가 힘들고 묵시적 JOIN 횟수가 늘어남에 따라 치명적인 성능 이슈가 발생할 수 있음 (묵시적 Join은 Inner Join만 가능)
- 컬렉션 값 연관 필드
- @OneToMany, @ManyToMany 관계에 있는 컬렉션이 대상
- 컬렉션이므로 경로 탐색을 이어나갈 수 없음
- 단, FROM 절에서 명시적 Join을 통해 Alias 획득 시 Alias를 통해 탐색 이어나갈 수 있음
- 단일 값 연관 필드와 마찬가지로 묵시적 Inner Join이 발생하므로 쿼리 튜닝 어려움
1.3 경로 표현식 올바른 예
- SELECT e.company.location FROM Employee e
- 성공이지만 묵시적 Join이 발생하므로 좋은 예시는 아님
- SELECT e.collections FROM Employee e
- 컬렉션 값 연관 필드
- 이 또한, 묵시적 Join 발생 (collections 값을 fetch 할 때 JOIN문 사용해야 함)
- SELECT m.username FROM Team t JOIN t.members m
- 컬렉션 값 연관 필드
- 원래는 컬렉션 값 연관 필드는 추가 경로 탐색이 안됨
- 하지만, Alias를 통해 별칭을 줬으므로 추가 탐색이 가능
- 또한, 명시적 Join을 했기 때문에 Join이 발생한다는 것을 확인할 수 있어 가독성이 좋음
1.4 경로 표현식 정리
- 묵시적 조인을 항상 조심
- 묵시적 조인은 Inner Join만 가능함
- 컬렉션에서 추가 탐색을 원한다면 Alias를 통해 별칭을 부여해야 함
- 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM절에 영향 끼침
- 결론은 가급적 명시적 Join만 사용 (Join은 SQL 튜닝의 핵심)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("hello"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Team team = new Team(); | |
team.setName("team"); | |
entityManager.persist(team); | |
Member member1 = new Member(); | |
member1.setUsername("관리자1"); | |
member1.setTeam(team); | |
entityManager.persist(member1); | |
Member member2 = new Member(); | |
member2.setUsername("관리자2"); | |
member2.setTeam(team); | |
entityManager.persist(member2); | |
entityManager.flush(); | |
entityManager.clear(); | |
/** | |
* select | |
* member0_.username as col_0_0_ | |
* from | |
* Member member0_ | |
*/ | |
// 상태 필드: username 이후로 탐색할 곳이 없음 (더 이상 탐색 X) | |
String query = "SELECT m.username FROM Member m"; | |
List<String> result = entityManager.createQuery(query, String.class) | |
.getResultList(); | |
// s = 관리자1,관리자2 | |
for (String s : result) { | |
System.out.println("s = " + s); | |
} | |
/** | |
* select | |
* team1_.id as id1_3_, | |
* team1_.name as name2_3_ | |
* from | |
* Member member0_ | |
* inner join | |
* Team team1_ | |
* on member0_.TEAM_ID=team1_.id | |
*/ | |
// 단일 값 연관 경로 (@ManyToOne, @OneToOne) | |
// 묵시적 INNER JOIN 발생 | |
// team 이후 name으로도 접근 가능 (name은 상태 필드) | |
String query2 = "SELECT m.team FROM Member m"; | |
List<Team> teams = entityManager.createQuery(query2, Team.class) | |
.getResultList(); | |
for (Team t : teams) { | |
System.out.println("team = " + t); | |
} | |
/** | |
* select | |
* members1_.id as id1_0_, | |
* members1_.age as age2_0_, | |
* members1_.memberType as memberty3_0_, | |
* members1_.TEAM_ID as team_id5_0_, | |
* members1_.username as username4_0_ | |
* from | |
* Team team0_ | |
* inner join | |
* Member members1_ | |
* on team0_.id=members1_.TEAM_ID | |
*/ | |
// 컬렉션 값 연관 경로 (@OneToMany, @ManyToMany) | |
// 묵시적 INNER JOIN 발생 | |
// 추가 탐색 X | |
String query3 = "SELECT t.members FROM Team t"; | |
Collection members = entityManager.createQuery(query3, Collection.class) | |
.getResultList(); | |
System.out.println("members = " + members); | |
/** | |
* select | |
* (select | |
* count(members1_.TEAM_ID) | |
* from | |
* Member members1_ | |
* where | |
* team0_.id=members1_.TEAM_ID) as col_0_0_ | |
* from | |
* Team team0_ | |
*/ | |
// membersSize = 2 | |
// 탐색 X, 리스트의 메서드 | |
String query4 = "SELECT t.members.size FROM Team t"; | |
Integer membersSize = entityManager.createQuery(query4, Integer.class) | |
.getSingleResult(); | |
System.out.println("membersSize = " + membersSize); | |
/** | |
* select | |
* members1_.username as col_0_0_ | |
* from | |
* Team team0_ | |
* inner join | |
* Member members1_ | |
* on team0_.id=members1_.TEAM_ID | |
*/ | |
// FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능 | |
String query5 = "SELECT m.username FROM Team t join t.members m"; | |
List<String> usernames = entityManager.createQuery(query5, String.class) | |
.getResultList(); | |
System.out.println("usernames = " + usernames); | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
e.printStackTrace(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} |
2. Fetch Join
- SQL의 조인 종류는 Inner Join, Outer Join, 그리고 Cross Join
- Fetch Join은 SQL 조인 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능
- Fetch Join은 연관된 엔티티나 컬렉션을 SQL을 통해 한번에 함께 조회하는 기능
- 즉, 지연 로딩일지라도 명시적 Join을 통해 한번에 불러오는 기능
- N+1 문제를 해결할 수 있는 방법
- N+1에 대해서는 추후 설명 예정
- ex) JPQL: SELECT e FROM Employee e JOIN FETCH e.company
- ex) SQL: SELECT e.*, c.* FROM Employee e INNER JOIN Company c ON e.company_id = c.id
- Lazy Loading으로 설정했더라도 Fetch Join을 통해 Eager Loading처럼 작동
- 정리하자면, Fetch Join은 객체 그래프를 SQL 한번 실행에 조회하는 개념
2.1 Fetch Join 예제
- 회원이 3명 있고 이 중 두 명은 팀 A, 한 명은 팀 B에 속한다고 가정
- 회원 내 팀은 @ManyToOne 관계이고 지연 로딩
- 이런 상황에서 팀들을 조회하고 팀의 @OneToMany 연관관계에 있는 회원 목록 내 회원의 이름과 팀명을 모두 출력할 때 Fetch Join을 사용하는 것과 사용하지 않는 것의 차이점은?
2.1.1 Fetch Join을 사용하지 않을 경우
- 회원 1, 팀 A를 조회하기 위해 묵시적 Join SQL 실행
- 회원 2, 팀 A
- 이미 팀 A는 PersistenceContext로 관리되기 때문에 1차 캐시에서 불러옴
- 별도 묵시적 Join SQL을 실행할 필요 없음
- 회원 3, 팀 B를 조회하기 위해 묵시적 Join SQL 실행
- 위와 같은 상황에서 회원 100명이 존재하고 각기 다른 팀일 경우 별도 쿼리 100번 호출 (성능 이슈)
- N + 1 문제 발생 [회원을 가져오기 위한 쿼리 1번 + 별도 쿼리 N번]
- 즉시 로딩을 하던 Lazy 로딩을 하던 전부 발생 => 이는 fetch join으로 해결해야 함
2.1.2 Fetch Join을 사용하는 경우
- 명시적 Join을 통해 즉시 로딩이던 지연 로딩이던 필요한 데이터를 한 번에 가져옴
- 루프를 돌 때 프록시가 아닌 진짜 데이터가 존재
- 따라서, 지연 로딩 없이 깔끔하게 쿼리 1번으로 필요한 데이터 들고 옴
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("hello"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Team teamA = new Team(); | |
teamA.setName("팀A"); | |
entityManager.persist(teamA); | |
Team teamB = new Team(); | |
teamB.setName("팀B"); | |
entityManager.persist(teamB); | |
Member member1 = new Member(); | |
member1.setUsername("회원1"); | |
member1.setTeam(teamA); | |
entityManager.persist(member1); | |
Member member2 = new Member(); | |
member2.setUsername("회원2"); | |
member2.setTeam(teamA); | |
entityManager.persist(member2); | |
Member member3 = new Member(); | |
member3.setUsername("회원3"); | |
member3.setTeam(teamB); | |
entityManager.persist(member3); | |
entityManager.flush(); | |
entityManager.clear(); | |
// LAZY이므로 일단 Member만 (Team X) | |
String query = "SELECT m FROM Member m"; | |
List<Member> members = entityManager.createQuery(query, Member.class) | |
.getResultList(); | |
for (Member m : members) { | |
// LAZY이므로 getTeam()이 호출될 때마다 아래의 쿼리가 호출됨 | |
/** | |
* select | |
* team0_.id as id1_3_0_, | |
* team0_.name as name2_3_0_ | |
* from | |
* Team team0_ | |
* where | |
* team0_.id=? | |
*/ | |
System.out.println("member = " + m.getUsername() + ", " + m.getTeam()); | |
/** | |
* 회원1, 팀A(SQL) | |
* 회원2, 팀A(PersistenceContext의 1차 캐시에서 불러옴, 즉 별도 쿼리 X) | |
* 회원3, 팀B(다른 팀이므로 SQL 또 날림) | |
* | |
* 회원 100명일 경우 최악의 경우 별도 쿼리 100번 (성능 이슈) | |
* -> N + 1 문제 발생 [회원을 가져오기 위한 쿼리 1번 + 별도 쿼리 N번] | |
* 즉시 로딩을 하던 Lazy 로딩을 하던 전부 발생 => 이는 fetch join으로 해결해야함 | |
*/ | |
} | |
/** | |
* select | |
* member0_.id as id1_0_0_, | |
* team1_.id as id1_3_1_, | |
* member0_.age as age2_0_0_, | |
* member0_.memberType as memberty3_0_0_, | |
* member0_.TEAM_ID as team_id5_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 | |
* | |
* JOIN을 통해 한번에 다 가져옴 | |
*/ | |
String fetchQuery = "SELECT m FROM Member m JOIN FETCH m.team"; | |
List<Member> fetchMembers = entityManager.createQuery(fetchQuery, Member.class) | |
.getResultList(); | |
for (Member m : fetchMembers) { | |
// 루프를 돌 떄 프록시가 아닌 진짜 데이터가 존재 | |
// 따라서, 지연로딩 없이 깔끔하게 쿼리 1번으로 필요한 데이터 들고 옴 | |
System.out.println("m = " + m.getUsername() + ", " + m.getTeam()); | |
} | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
e.printStackTrace(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} |
2.2 컬렉션 Fetch Join일 경우 데이터 양이 실제보다 많이 나오는 문제
- Fetch Join을 통해 [N+1] 문제를 해결한 것은 확인
- 하지만, 이상한 점은 팀은 기준으로 출력했는데도 결과가 3개였다는 점
- 회원 1, 팀 A
- 회원 2, 팀 A
- 회원 3, 팀 B
- 예상하기로는 팀 A, 팀 B 단 둘일 뿐이므로 컬렉션 사이즈가 2
- PersistenceContext에서는 팀 A가 하나로 인식이 되지만 팀 내 회원 Collection에는 팀 A가 두 개가 존재하는 것이 문제
- 팀 A, 팀 B 두 개만 출력하기를 원한다면 SQL의 Distinct 키워드를 떠올릴 수 있음
- 하지만, 회원 1, 회원 2의 이름이 다르므로 일반적인 SQL의 Distinct로는 팀 A가 두 번 출력되는 것을 방지할 수 없음
- 다행히도 JPA에서는 Distinct 키워드 부여 시 영속성 컨텍스트 내 같은 식별자를 가진 Team 엔티티 제거
- 따라서, 원하는 결과를 위해서는 Distinct 키워드를 붙여야 함
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("hello"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Team teamA = new Team(); | |
teamA.setName("팀A"); | |
entityManager.persist(teamA); | |
Team teamB = new Team(); | |
teamB.setName("팀B"); | |
entityManager.persist(teamB); | |
Member member1 = new Member(); | |
member1.setUsername("회원1"); | |
member1.setTeam(teamA); | |
entityManager.persist(member1); | |
Member member2 = new Member(); | |
member2.setUsername("회원2"); | |
member2.setTeam(teamA); | |
entityManager.persist(member2); | |
Member member3 = new Member(); | |
member3.setUsername("회원3"); | |
member3.setTeam(teamB); | |
entityManager.persist(member3); | |
entityManager.flush(); | |
entityManager.clear(); | |
/** | |
* 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_.memberType as memberty3_0_1_, | |
* members1_.TEAM_ID as team_id5_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 | |
*/ | |
// 1:다 join은 뻥튀기될 가능성이 있음 | |
String teamQuery = "SELECT t FROM Team t JOIN FETCH t.members"; | |
List<Team> teams = entityManager.createQuery(teamQuery, Team.class) | |
.getResultList(); | |
/** | |
* team = 팀A|members = 2 | |
* m = Member{id=4, username='회원1', age=0, team=com.tistory.jaimemin.jpql.Team@57540fd0, memberType=null} | |
* m = Member{id=5, username='회원2', age=0, team=com.tistory.jaimemin.jpql.Team@57540fd0, memberType=null} | |
* team = 팀A|members = 2 | |
* m = Member{id=4, username='회원1', age=0, team=com.tistory.jaimemin.jpql.Team@57540fd0, memberType=null} | |
* m = Member{id=5, username='회원2', age=0, team=com.tistory.jaimemin.jpql.Team@57540fd0, memberType=null} | |
* team = 팀B|members = 1 | |
* m = Member{id=6, username='회원3', age=0, team=com.tistory.jaimemin.jpql.Team@3a627c80, memberType=null} | |
*/ | |
// 왜 teamA가 두 번 출력될까? | |
for (Team team : teams) { | |
System.out.println("team = " + team.getName() + "|members = " + team.getMembers().size()); | |
for (Member m : team.getMembers()) { | |
System.out.println("m = " + m); | |
} | |
} | |
/** | |
* 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_.memberType as memberty3_0_1_, | |
* members1_.TEAM_ID as team_id5_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 | |
*/ | |
// SQL의 DISTINCT 키워드 만으로는 distinct 안됨 | |
// JPA에서는 같은 식별자를 가진 Team 엔티티 제거 | |
String distinctQuery = "SELECT DISTINCT t FROM Team t JOIN FETCH t.members"; | |
List<Team> distinctTeams = entityManager.createQuery(distinctQuery, Team.class) | |
.getResultList(); | |
/** | |
* team = 팀A|members = 2 | |
* m = Member{id=4, username='회원1', age=0, team=com.tistory.jaimemin.jpql.Team@57540fd0, memberType=null} | |
* m = Member{id=5, username='회원2', age=0, team=com.tistory.jaimemin.jpql.Team@57540fd0, memberType=null} | |
* team = 팀B|members = 1 | |
* m = Member{id=6, username='회원3', age=0, team=com.tistory.jaimemin.jpql.Team@3a627c80, memberType=null} | |
*/ | |
for (Team team : distinctTeams) { | |
System.out.println("team = " + team.getName() + "|members = " + team.getMembers().size()); | |
for (Member m : team.getMembers()) { | |
System.out.println("m = " + m); | |
} | |
} | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
e.printStackTrace(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} |
* @OneToMany 관계의 경우 위와 같은 문제가 발생할 수 있음
* 반면, @ManyToOne의 경우 데이터가 뻥튀기되는 것을 걱정하지 않아도 됨
2.3 Fetch Join의 특징과 한계
- Fetch Join 대상에는 Alias를 통해 별칭을 부여할 수 없음
- Fetch Join의 경우 연관된 모든 field를 조회
- Alias 조작하다 일부 데이터 누락한 상태로 데이터 변경이 이루어질 수 있음
- 일부만 가져오고 싶을 경우 별도 쿼리를 실행하는 것을 추천
- Hibernate에서는 Fetch Join 대상에도 Alias를 부여할 수 있도록 하지만 가급적 사용 X
- 하나의 컬렉션에 대해서만 Fetch Join 가능
- 컬렉션을 Fetch Join 할 경우 Paging API 사용 불가
- 앞서 1:다 관계의 경우 데이터 뻥튀기가 가능하므로 페이징 X
- 반면, 1:1, 다:1와 같이 단일 값 연관 필드들은 Fetch Join을 해도 페이징 API 지원
- @BatchSize 어노테이션을 적용할 경우 해결 가능
- @BatchSize는 컬렉션을 조회할 때 한 번에 몇 개의 데이터를 가져오는지 설정하는 어노테이션 (1,000 이하로 설정하는 것을 권장)
- @BatchSize 어노테이션을 통해 LAZY 로딩 시 BatchSize만큼 넘기기 때문에 쿼리 3개가 아닌 2개만 날림
- @BatchSize는 [N+1] 문제의 해결방법 중 하나
- Fetch Join은 누차 강조하지만 연관된 엔티티들을 SQL 한 번 실행으로 조회하는 성능 최적화 기능
- 실무에서는 글로벌 로딩 전략을 기본적으로 Lazy Loading으로 설정하고 최적화가 필요한 곳은 Fetch Join을 적용하여 해결하는 방식을 추천
- 대부분의 JPA 성능 이슈는 [N + 1] 문제에서 발생하므로 Fetch Join과 @BatchSize는 알고 있어야 함
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("hello"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Team teamA = new Team(); | |
teamA.setName("팀A"); | |
entityManager.persist(teamA); | |
Team teamB = new Team(); | |
teamB.setName("팀B"); | |
entityManager.persist(teamB); | |
Member member1 = new Member(); | |
member1.setUsername("회원1"); | |
member1.setTeam(teamA); | |
entityManager.persist(member1); | |
Member member2 = new Member(); | |
member2.setUsername("회원2"); | |
member2.setTeam(teamA); | |
Member member3 = new Member(); | |
member3.setUsername("회원3"); | |
member3.setTeam(teamB); | |
entityManager.persist(member3); | |
entityManager.persist(member2); | |
entityManager.flush(); | |
entityManager.clear(); | |
// fetch join의 경우 페이징 API 적용이 안됨 | |
String pagingQuery = "SELECT t FROM Team t"; | |
List<Team> pagingTeams = entityManager.createQuery(pagingQuery, Team.class) | |
.setFirstResult(0) | |
.setMaxResults(2) | |
.getResultList(); | |
for (Team team : pagingTeams) { | |
System.out.println("team = " + team.getName() + "|members = " + team.getMembers().size()); | |
// LAZY여서 성능이 안 나옴 | |
// @BatchSize 어노테이션을 통해 LAZY 로딩 시 BatchSize만큼 넘기기 때문에 | |
// 쿼리 3개가 아닌 2개만 날림 (TeamA와 TeamB에 대해) | |
// [N + 1] 문제 해결법 중 하나 | |
for (Member m : team.getMembers()) { | |
System.out.println("m = " + m); | |
} | |
} | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
e.printStackTrace(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} | |
@Entity | |
public class Team { | |
@Id | |
@GeneratedValue | |
private Long id; | |
private String name; | |
@BatchSize(size = 100) | |
@OneToMany(mappedBy = "team") | |
private List<Member> members = new ArrayList<>(); | |
public Long getId() { | |
return id; | |
} | |
public void setId(Long id) { | |
this.id = id; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public List<Member> getMembers() { | |
return members; | |
} | |
} |
2.4 Fetch Join 정리
- Fetch Join은 객체 그래프를 유지할 때 효과적
- 즉, 모든 연관 필드를 찾아갈 때 유리
- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 할 경우, Fetch Join을 통해 모든 연관 필드를 불러오는 것은 비효율적
- 명시적 Join을 사용하여 필요한 데이터들만 조회해서 별도로 생성한 DTO로 반환하는 것이 효과적
3. 엔티티 파라미터
- JPQL은 테이블 중심이 아닌 엔티티 중심
- 따라서, 파라미터로 엔티티를 직접 사용해도 됨
- 이를 SQL로 변환하면 결국에는 엔티티의 primary key 즉, 기본키를 파라미터로 넘긴다는 것을 알 수 있음
- 엔티티를 식별하는 것이 기본키이기 때문
- ex) JPQL: SELECT COUNT(e) FROM Employee e
- ex) SQL: SELECT COUNT(e.id) FROM Employee e
- 따라서, 파라미터 바인딩을 통해 엔티티 자체를 넘길 수가 있음
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("hello"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Team teamA = new Team(); | |
teamA.setName("팀A"); | |
entityManager.persist(teamA); | |
Team teamB = new Team(); | |
teamB.setName("팀B"); | |
entityManager.persist(teamB); | |
Member member1 = new Member(); | |
member1.setUsername("회원1"); | |
member1.setTeam(teamA); | |
entityManager.persist(member1); | |
Member member2 = new Member(); | |
member2.setUsername("회원2"); | |
member2.setTeam(teamA); | |
entityManager.persist(member2); | |
Member member3 = new Member(); | |
member3.setUsername("회원3"); | |
member3.setTeam(teamB); | |
entityManager.persist(member3); | |
entityManager.flush(); | |
entityManager.clear(); | |
/** | |
* select | |
* team0_.id as id1_3_0_, | |
* team0_.name as name2_3_0_ | |
* from | |
* Team team0_ | |
* where | |
* team0_.id=? | |
*/ | |
// 엔티티 자체를 파라미터로 넘길 수 있음 | |
String query = "SELECT m FROM Member m WHERE m = :member"; | |
Member foundMember = entityManager.createQuery(query, Member.class) | |
.setParameter("member", member1) | |
.getSingleResult(); | |
System.out.println("foundMember = " + foundMember); | |
/** | |
* select | |
* team0_.id as id1_3_0_, | |
* team0_.name as name2_3_0_ | |
* from | |
* Team team0_ | |
* where | |
* team0_.id=? | |
*/ | |
// 파라미터 바인딩 | |
String foreignKeyQuery = "SELECT m FROM Member m WHERE m.team = :team"; | |
Member foundTeamMember = entityManager.createQuery(foreignKeyQuery, Member.class) | |
.setParameter("team", teamB) | |
.getSingleResult(); | |
System.out.println("foundTeamMember = " + foundTeamMember); | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
e.printStackTrace(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} |
4. Named Query
- 엔티티에 미리 정의해서 별도 이름을 부여해두고 사용하는 JPQL
- 정적 쿼리이며 어노테이션으로 정의
- XML로도 정의할 수 있지만 XML은 거의 사용하지 않음
- 애플리케이션 로딩 시점에 초기화 후 재사용 가능하다는 것이 장점
- 로딩 시점에 애플리케이션이 SQL을 파싱 하고 캐싱하기 때문에 성능 최적화
- 애플리케이션 로딩 시점에 쿼리를 검증
- 런타임 에러를 통해 장애를 방지 가능
- 런타임 에러기 때문에 애플리케이션을 실행했을 때 SQL grammar 에러를 파악 가능
- Entity 클래스가 지저분해지는 단점이 존재
- 이는 Spring Data JPA를 적용할 경우 해결 가능
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("hello"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Team teamA = new Team(); | |
teamA.setName("팀A"); | |
entityManager.persist(teamA); | |
Team teamB = new Team(); | |
teamB.setName("팀B"); | |
entityManager.persist(teamB); | |
Member member1 = new Member(); | |
member1.setUsername("회원1"); | |
member1.setTeam(teamA); | |
entityManager.persist(member1); | |
Member member2 = new Member(); | |
member2.setUsername("회원2"); | |
member2.setTeam(teamA); | |
entityManager.persist(member2); | |
Member member3 = new Member(); | |
member3.setUsername("회원3"); | |
member3.setTeam(teamB); | |
entityManager.flush(); | |
entityManager.clear(); | |
/** | |
* select | |
* member0_.id as id1_0_, | |
* member0_.age as age2_0_, | |
* member0_.memberType as memberty3_0_, | |
* member0_.TEAM_ID as team_id5_0_, | |
* member0_.username as username4_0_ | |
* from | |
* Member member0_ | |
* where | |
* member0_.username=? | |
*/ | |
Member foundMember = entityManager.createNamedQuery("Member.findByUsername", Member.class) | |
.setParameter("username", "회원1") | |
.getSingleResult(); | |
System.out.println("foundMember = " + foundMember); | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
e.printStackTrace(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} | |
@Entity | |
@Getter | |
@NamedQuery( | |
name = "Member.findByUsername", | |
query = "SELECT m FROM Member m WHERE m.username = :username" | |
) | |
public class Member { | |
@Id | |
@GeneratedValue | |
private Long id; | |
private String username; | |
private int age; | |
@ManyToOne(fetch = FetchType.LAZY) | |
@JoinColumn(name = "TEAM_ID") // 외래키 값 | |
private Team team; | |
@Enumerated(EnumType.STRING) | |
private MemberType memberType; | |
public void setTeam(Team team) { | |
this.team = team; | |
team.getMembers().add(this); | |
} | |
public void setId(Long id) { | |
this.id = id; | |
} | |
public void setUsername(String username) { | |
this.username = username; | |
} | |
public void setAge(int age) { | |
this.age = age; | |
} | |
public void setMemberType(MemberType memberType) { | |
this.memberType = memberType; | |
} | |
@Override | |
public String toString() { | |
return "Member{" + | |
"id=" + id + | |
", username='" + username + '\'' + | |
", age=" + age + | |
", team=" + team + | |
", memberType=" + memberType + | |
'}'; | |
} | |
} |
5. 벌크 연산
- JPA는 벌크성보다는 단발성에 좀 더 최적화되어 있지만 벌크 연산도 지원
- 직원들의 연봉을 일괄 200만 원 인상하는 쿼리를 실행할 경우 직원의 수만큼 SQL을 호출해야 함
- 이는 DB에 부하를 줄 수 있음 (운영팀에서 안 좋아할 것이 분명함)
- 이때 등장하는 개념이 벌크 연산
- 벌크 연산은 쿼리 한 번으로 여러 테이블의 로우를 변경
- executeUpdate() 메서드를 통해 벌크 연산이 가능하며 리턴 값은 영향받은 엔티티 수 반환
- UPDATE 및 DELETE에 주로 쓰이며 Hibernate을 통해 INSERT에서도 가능
- 주의할 점은 벌크 연산은 PersistenceContext를 거치지 않고 DB에 직접 쿼리를 실행
- 이는 영속성 컨텍스트에 아무 작업도 하지 않고 바로 벌크 연산을 먼저 실행하면서 해결도 가능하고
- 벌크 연산 수행 후 영속성 컨텍스트를 초기화하는 방식으로도 해결 가능
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("hello"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Team teamA = new Team(); | |
teamA.setName("팀A"); | |
entityManager.persist(teamA); | |
Team teamB = new Team(); | |
teamB.setName("팀B"); | |
entityManager.persist(teamB); | |
Member member1 = new Member(); | |
member1.setUsername("회원1"); | |
member1.setTeam(teamA); | |
entityManager.persist(member1); | |
Member member2 = new Member(); | |
member2.setUsername("회원2"); | |
member2.setTeam(teamA); | |
entityManager.persist(member2); | |
Member member3 = new Member(); | |
member3.setUsername("회원3"); | |
member3.setTeam(teamB); | |
entityManager.persist(member3); | |
// entityManager.flush(); | |
// entityManager.clear(); | |
// 전부 20살로 업데이트 | |
// persist 했으므로 FLUSH는 된 상황 | |
int resultCount = entityManager.createQuery("UPDATE Member m SET m.age = 20") | |
.executeUpdate(); | |
System.out.println("resultCount = " + resultCount); | |
// clear 안했으므로 영속성 컨텍스트에는 반영 안되어있음 | |
System.out.println("member1.getAge() = " + member1.getAge()); | |
System.out.println("member2.getAge() = " + member1.getAge()); | |
System.out.println("member3.getAge() = " + member1.getAge()); | |
// 따라서, 영속성 컨텍스트 초기화해주는 것이 중요 | |
entityManager.clear(); | |
Member foundMember = entityManager.find(Member.class, member1.getId()); | |
System.out.println("foundMember.getAge() = " + foundMember.getAge()); | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
e.printStackTrace(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} |
출처
자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한 강사님)
반응형
'DB > JPA' 카테고리의 다른 글
[JPA] Hibernate MultipleBagFetchException (0) | 2023.06.28 |
---|---|
[JPA] 준영속(Detached) 상태 엔티티 수정하는 방법 (0) | 2023.05.07 |
[JPA] JPQL 간단 정리 (0) | 2021.10.16 |
[JPA] 값 타입 정리 (0) | 2021.09.29 |
[JPA] 프록시와 연관관계 관리 정리 (0) | 2021.09.14 |