DB/JPA

[JPA] JPQL 추가 정리

꾸준함. 2021. 10. 18. 02:56

개요

지난 게시글(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 튜닝의 핵심)

 

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();
}
view raw .java hosted with ❤ by GitHub

 

 

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번으로 필요한 데이터 들고 옴

 

 

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();
}
view raw .java hosted with ❤ by GitHub

 

 

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 키워드를 붙여야 함

 

 

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();
}
view raw .java hosted with ❤ by GitHub

 

* @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는 알고 있어야 함

 

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;
}
}
view raw .java hosted with ❤ by GitHub

 

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
  • 따라서, 파라미터 바인딩을 통해 엔티티 자체를 넘길 수가 있음


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();
}
view raw .java hosted with ❤ by GitHub

 

4. Named Query

  • 엔티티에 미리 정의해서 별도 이름을 부여해두고 사용하는 JPQL
  • 정적 쿼리이며 어노테이션으로 정의
    • XML로도 정의할 수 있지만 XML은 거의 사용하지 않음
  • 애플리케이션 로딩 시점에 초기화 후 재사용 가능하다는 것이 장점
    • 로딩 시점에 애플리케이션이 SQL을 파싱 하고 캐싱하기 때문에 성능 최적화
  • 애플리케이션 로딩 시점에 쿼리를 검증
    • 런타임 에러를 통해 장애를 방지 가능
    • 런타임 에러기 때문에 애플리케이션을 실행했을 때 SQL grammar 에러를 파악 가능
  • Entity 클래스가 지저분해지는 단점이 존재
    • 이는 Spring Data JPA를 적용할 경우 해결 가능


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 +
'}';
}
}
view raw .java hosted with ❤ by GitHub

 

5. 벌크 연산

  • JPA는 벌크성보다는 단발성에 좀 더 최적화되어 있지만 벌크 연산도 지원
  • 직원들의 연봉을 일괄 200만 원 인상하는 쿼리를 실행할 경우 직원의 수만큼 SQL을 호출해야 함
    • 이는 DB에 부하를 줄 수 있음 (운영팀에서 안 좋아할 것이 분명함)
    • 이때 등장하는 개념이 벌크 연산
  • 벌크 연산은 쿼리 한 번으로 여러 테이블의 로우를 변경
  • executeUpdate() 메서드를 통해 벌크 연산이 가능하며 리턴 값은 영향받은 엔티티 수 반환
  • UPDATE 및 DELETE에 주로 쓰이며 Hibernate을 통해 INSERT에서도 가능
  • 주의할 점은 벌크 연산은 PersistenceContext를 거치지 않고 DB에 직접 쿼리를 실행
    • 이는 영속성 컨텍스트에 아무 작업도 하지 않고 바로 벌크 연산을 먼저 실행하면서 해결도 가능하고
    • 벌크 연산 수행 후 영속성 컨텍스트를 초기화하는 방식으로도 해결 가능


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();
}
view raw .java hosted with ❤ by GitHub

 

출처

자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한 강사님)

반응형