JPA 11th Step

최보현·2022년 9월 19일
0

JPA

목록 보기
10/10
post-thumbnail

자바 ORM 표준 JPA 프로그래밍 - 기본편 - sec11
출처 : JPA 기본편

JPQL 중급 문법

경로 표현식

.을 찍어 객체 그래프를 탐색하는 것

select m.username -> 상태 필드
엔티티안에서 값을 바로 찍어내는 것
from Member m
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'

  • 상태 필드(state field): 단순히 값을 저장하기 위한 필드 ex: m.username
  • 연관 필드(association field): 연관관계를 위한 필드
    • 단일 값 연관 필드:
    @ManyToOne, @OneToOne, 대상이 엔티티 ex: m.team
    • 컬렉션 값 연관 필드:
    @OneToMany, @ManyToMany, 대상이 컬렉션 ex: m.orders

특징

  • 상태 필드: 경로 탐색의 끝, 탐색❌

    JPQL: select m.username, m.age from Member m
    SQL: select m.username, m.age from Member m

  • 단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색O

String query = "select m.team From Member m";

List<Team> result = em.createQuery(query, Team.class)
		.getResultList();

  • 컬렉션 값 연관 경로: 묵시적 내부 조인 발생, 탐색❌
String query = "select t.members From Team t";

Collection result = em.createQuery(query, Collection.class)
		.getResultList();


size같은 경우에는 가져올 수 있으나 나머지들은 .을 찍어도 나오는게 없음

join의 경우 성능 튜닝에 지대한 영향을 줌 그래서 막 join이 되면 안되게 관리 해야 함
• FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
"select m.username From Team t join t.members m"

명시적 조인, 묵시적 조인

  • 명시적 조인: join 키워드 직접 사용
    select m from Member m join m.team t
  • 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능)
    select m.team from Member m

주의사항

항상 내부 조인

  • 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야함
  • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 줌
    가급적 묵시적 조인 대신에 명시적 조인 사용
    WHY? 조인은 SQL 튜닝에 중요 포인트이고 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

예시

select o.member.team from Order o -> 성공 => join이 2번 일어남
select t.members from Team -> 성공
select t.members.username from Team t -> 실패 => 컬렉션에서는 더 들어갈 수 없음 from 별칭을 사용하면 모를까
select m.username from Team t join t.members m -> 성공

페치 조인 - 기본

페치 조인이란?

SQL 조인 종류X

  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능 => 쿼리 2번 나갈 거를 한 번에
  • join fetch 명령어 사용
  • 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로

엔티티 페치 조인

회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)하도록 만들었을 때 SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT

• [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

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 jpql = "select m from Member m";
List<Member> members = em.createQuery(jpql, Member.class)
			.getResultList();
            
for (Member member : members) {
  System.out.println("username = " + member.getUsername() + ", " + "teamName = " + member.getTeam().name());
}

/* 결과값
username = 회원1, teamname = 팀A
username = 회원2, teamname = 팀A
username = 회원3, teamname = 팀B
*/


첫 번재 루프를 돌면서 회원1꺼를 가져오게 되겠죠~ 프록시가 .getName()으로 값을 직접 사용하는 순간에 영속성 컨텍스트에 내놔라고 하면서 team을 조회하는 쿼리가 나가게 됨(이 순간에 teamA가 영속성 컨텍스트에 들어가게 됨)
회원2도 1이랑 같은 팀이고 teamA는 이제 1차 캐시에 있으니깐 따로 쿼리문이 나가지 않고 바로 가져오게 됨
회원3는 teamB인데 이 팀은 아직 영속성에 없어서 회원1이 겪은 상황을 동일하게 겪게 됨

=> N(팀을 찾기 위한 쿼리)+1(회원을 찾기 위해 날리는 맨 첫 번째 쿼리) 문제 발생

이를 위한 해결 방법이 페치 조인

String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
			.getResultList();
            
for (Member member : members) {
//페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X 페치 조인이 우선
  System.out.println("username = " + member.getUsername() + ", " + "teamName = " + member.getTeam().name());
}


팀의 쿼리가 따로 안나감, 애초에 팀이랑 멤버를 다 들고 왔음 => 쿼리가 날라가서 result에 담기는 순간에 team은 프록시가 아니라 진짜 데이터가 담김

컬렉션 페치 조인

일대다 관계, 컬렉션 페치 조인

• [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'

String jpql = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(jpql, Team.class)
			.getResultList();
            
for (Team team : result) {
  System.out.println("team = " + member.getName() + "|members= " + team.getMembers().size());
}

/*결과값
team = 팀A|members=2
team = 팀A|members=2
team = 팀B|members=1

데이터는 가져오지만 뻥튀기 발생

String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();

for(Team team : teams) {
  System.out.println("teamname = " + team.getName() + ", team = " + team);
  for (Member member : team.getMembers()) {
  //페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
    System.out.println(-> username = " + member.getUsername()+ ", member = " + member);
  }
}

/* 결과값
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
*/

DISTINCT

SQL의 DISTINCT는 중복된 결과를 제거하는 명령
JPQL의 DISTINCT 2가지 기능 제공
1. SQL에 DISTINCT를 추가
2. 애플리케이션에서 엔티티 중복 제거
select distinct t from Team t join fetch t.members where t.name = ‘팀A’ 이렇게 했을 때, SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과
에서 중복제거 실패! (정말 모든게 일치해야 데이터베이스에서 삭제를 해줌)

그래서! DISTINCT가 추가로 애플리케이션에서 중복 제거시도 => 같은 식별자를 가진 Team 엔티티 제거

반대로 다대일은 뻥튀기가 안됨(Member에서 team으로 가는 경우)

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

일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음
<일반 조인>

String query = select t from Team t join t.members m


team만 가져오는 걸 볼 수 있음 + 데이터 뻥튀기도 됨
또한 컬렉션은 프록시가 아니지만 데이터가 로딩 시점에 로딩이 안되있기 때문에 members를 조회하는 쿼리가 또 나가게 됨

  • JPQL은 결과를 반환할 때 연관관계 고려 ❌
  • 단지 SELECT 절에 지정한 엔티티만 조회할 뿐 => 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회 ❌
  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

페치 조인은 연관된 엔티티를 함께 조회함
<페치 조인>

select t from Team t join fetch t.members

쿼리 한 번에 select절에 모든 내용을 담아서 나가서 우리가 원하는 깔끔한 결과 완성

페치 조인 - 특징과 한계

  • 페치 조인 대상에는 별칭을 줄 수 없음
    select t From Team t join fetch t.members as m where m.members 뭐시기 안됨"
    => 페치 조인은 나와 연관된 애들을 모두 다 끌고 오겠다는 것임, 근데 거기서 걸러서 몇 개만 가져오고 싶다? 그러면 join fetch를 쓰면 안되고 where절 같은 걸로 따로 조회해야 함
    ex) 팀에 멤버가 5명인데 그 중 1명만 가져온다, 4명 누락되있는 상태에서 잘못 작동할 수도 있음
    • 하이버네이트는 가능, 가급적 사용 ❌
  • 둘 이상의 컬렉션은 페치 조인 불가
    => 일대다도 뻥튀기가 되는 상황에 이건 일대다대다 상황이기 때문에 곱하기 곱하기가 일어나 버림
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용 불가
    => ex) 위에 예제에서 만약에 결과값 하나만 가져와라고 했을 때, teamA에 회원1밖에 없다는 결과가 나올 것임, 실제는 회원2까지 있는데 문제 발생!
String jpql = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(jpql, Team.class)
			.setFirstResult(0)
            .setMaxResult(1)
            .getResultList();

WARN: HHH000104: firstResult/MaxResults specified with collection fetch; applying in memory!

페이징 쿼리가 나가지 않음 => DB에서 이 팀에 대한 쿼리를 다 끌고옴
• 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
• 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)

할 수 있는 방법

  1. 뒤집기
    query = "select m From Member m join fetch m.team t";
  2. join fetch 빼기
String query = "select t from Team t";

for (Team team : result) {
  System.out.println("team = " + member.getName() + "|members= " + team.getMembers().size());
}


우선 팀을 찾는 쿼리를 내보냄

루프를 돌면서 팀과 관련된 멤버들을 lazy로딩을 통해서 가져옴 그래서 팀 B까지 해서 총 3번의 쿼리가 나가게 됨
=> 성능이 안 나옴, 불필요하게 쿼리가 많이 나게 되니깐
3. BetchSize(size=@)
레이지 로딩을 통해서 team을 가져올 때 betchsize에서 적은 숫자만큼 한 번에 끌고 옴
4. 글로벌 세팅
persistence.xml에 hibernate.default_batch_fetch_size value=100 이런 식으로 적용하면 3번과 똑같은 효과를 낼 수 있음

  • 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
    • @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
  • 최적화가 필요한 곳은 페치 조인 적용

페치 조인 - 최종 정리

  • 모든 것을 페치 조인으로 해결할 불가
  • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적 => m member. 이렇게 경로 탐색을 해서 할 때는 좋음
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적

다형성 쿼리

TYPE

조회 대상을 특정 자식으로 한정하고자 할 때 ex) Item 중에 Book, Movie를 조회

• [JPQL]
select i from Item i
where type(i) IN (Book, Movie)
• [SQL]
select i from i
where i.DTYPE in (‘B’, ‘M’)

TREAT

자바의 타입 캐스팅과 유사

  • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
  • FROM, WHERE, SELECT(하이버네이트 지원) 사용
    ex) 부모인 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’

엔티티 직접 사용

기본 키 값

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용

• [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

//엔티티를 파라미터로 전달
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=?
*/

외래 키 값

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=?
*/

Named 쿼리 - 정적 쿼리

미리 정의해서 이름을 부여해두고 사용하는 JPQL

  • 정적 쿼리
  • 어노테이션, XML에 정의
    <어노테이션 정의>
@Entity
@NamedQuery(
    name = "Member.findByUsername",
    query="select m from Member m where m.username = :username")
  public class Member {
}

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

<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>
  • 애플리케이션 로딩 시점에 초기화 후 재사용
  • 애플리케이션 로딩 시점에 쿼리를 검증
  • XML이 항상 우선권을 가짐
  • 애플리케이션 운영 환경에 따라 다른 XML 배포 가능

벌크 연산

특정 조건을 충족하는 몇 개들만 업데이트를 하고자 할 때 ex) 재고가 10개 미만인 모든 상품의 가격을 10% 상승하고자 할 때

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

벌크 연산 예제

쿼리 한 번으로 여러 테이블 로우 변경(엔티티)

  • executeUpdate()의 결과는 영향받은 엔티티 수 반환
  • UPDATE, DELETE 지원
  • INSERT(insert into .. select, 하이버네이트 지원)
String qlString = "update Product p " +
                  "set p.price = p.price * 1.1 " +
                  "where p.stockAmount < :stockAmount";

int resultCount = em.createQuery(qlString)
    .setParameter("stockAmount", 10)
    .executeUpdate();

주의할 점

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리
• 벌크 연산을 먼저 실행
벌크 연산 수행 후 영속성 컨텍스트 초기화

profile
Novice Developer's Blog

0개의 댓글