[JPA] Chapter 10. 객체지향 쿼리 언어 4 - 객체지향 쿼리 심화

joyful·2021년 9월 4일
0

JPA

목록 보기
17/18

들어가기 앞서

이 글은 김영한 님의 저서 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.


10.5 객체지향 쿼리 심화

10.5.1 벌크 연산

여러 건을 한 번에 수정하거나 삭제하는 연산

  • 기존의 수정 및 삭제 방법 사용 시, 수백 개 이상의 엔티티를 처리하는 데 소요되는 시간이 클 때 사용
  • executeUpdate() : 벌크 연산으로 영향을 받은 엔티티 건수 반환

💻 UPDATE 벌크 연산

/*재고가 10개 미만인 모든 상품 가격 10% 상승*/
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();

💻 DELETE 벌크 연산

/*가격이 100원 미만인 상품 삭제*/
String qlString =
    "delete from Product p " +
    "where p.price < :price";
    
int resultCount = em.createQuery(qlString)
                    .setParameter("price", 100)
                    .executeUpdate();

📖 참고

하이버네이트에서 INSERT 벌크 연산 지원함(JPA 표준 x)


✅ 벌크 연산의 주의점

  • 사용 시 영속성 컨텍스트 무시 및 데이터베이스에 직접 쿼리

💻 예제

//상품A 조회(상품A의 가격: 1000원)
//조회된 상품A → 영속성 컨텍스트에서 관리
Product productA =
    em.createQuery("select p from Product p where p.name = :name", Product.class);
      .setParameter("name", "productA")
      .getSingleResult();
      
//출력 결과: 1000
System.out.println("productA 수정 전 = " + productA.getPrice();

//벌크 연산 수행으로 모든 상품 가격 10% 상승
//예상 결과값 : 1100원
em.createQuery("update Product p set p.price = p.price * 1.1")
  .executeQuery();

//출력 결과: 1000
//예상 결과값인 1100원과 다름
System.out.println("productA 수정 후 = " + productA.getPrice());

📊 벌크 연산 전

  • 상품A 조회 → 가격이 1000원인 상품A가 영속성 컨텍스트에서 관리

📊 벌크 연산 수행 후

  • 영속성 컨텍스트를 통하지 않고 데이터베이스에 직접 쿼리
    → 영속성 컨텍스트와 데이터베이스 간의 데이터 불일치

📝 해결 방법

  1. em.refresh() 사용
em.refresh(productA);  //데이터베이스에서 상품A 다시 조회
  1. 벌크 연산 먼저 실행

    • 가장 실용적인 방법
    • 벌크 연산 먼저 실행 후 데이터 조회시, 벌크 연산으로 변경된 데이터 조회
    • JPA와 JDBC 함께 사용시에도 유용
  2. 벌크 연산 수행 후 영속성 컨텍스트 초기화

    • 이후 엔티티 조회 시 벌크 연산이 적용된 데이터베이스에서 엔티티 조회
    • 미적용 시 영속성 컨텍스트에 남아 있는 엔티티에는 벌크 연산이 적용되어 있지 않음

  • 벌크 연산은 영속성 컨텍스트와 2차 캐시를 무시하고 데이터베이스에 직접 실행
    → 영속성 컨텍스트와 데이터베이스 간에 데이터 차이 발생 가능성 존재
  • 벌크 연산을 먼저 수행하고 상황에 따라 영속성 컨텍스트 초기화(권장)


10.5.2 영속성 컨텍스트와 JPQL

✅ 쿼리 후 영속 상태인 것과 아닌 것

  • 조회한 엔티티만 영속성 컨텍스트가 관리
select m from Member m  //엔티티 조회(관리O)
select o.address from Order o  //임베디드 타입 조회(관리 x)
select m.id, m.username from Member m  //단순 필드 조회(관리 x)

✅ JPQL로 조회한 엔티티와 영속성 컨텍스트

em.find(Member.class, "member1");  //회원1 조회

//엔티티 쿼리 조회 결과가 회원1, 회원2
List<Member> resultList =
    em.createQuery("select m from Member m", Member.class)
      .getResultList();
  • JPQL로 데이터베이스에서 조회한 엔티티가 영속성 컨텍스트에 이미 존재하는 경우
    • JPQL로 데이터베이스에서 조회한 결과를 버리고 영속성 컨텍스트에 있던 엔티티 반환
    • 식별자 값을 사용하여 비교

📊 엔티티 쿼리 1 - 엔티티 쿼리와 결과

📊 엔티티 쿼리 2 - 결과 비교와 리턴

  1. JPQL을 사용하여 조회 요청
  2. JPQL이 SQL로 변환되어 데이터베이스 조회
  3. 조회한 결과와 영속성 컨텍스트 비교
  4. 식별자 값 기준으로 member1은 영속성 컨텍스트에 존재하므로 버리고, 기존에 존재하는 member1이 반환 대상이 됨
  5. 식별자 값 기준으로 member2는 영속성 컨텍스트에 존재하지 않으므로 영속성 컨텍스트에 추가
  6. 쿼리 결과인 member1, member2 반환

  • JPQL로 조회한 엔티티는 영속 상태
  • 영속성 컨텍스트에 이미 엔티티 존재시 기존 엔티티 반환

    영속성 컨텍스트는 영속 상태인 엔티티의 동일성 보장


find() vs JPQL

📝 find()

  • 엔티티를 영속성 컨텍스트에서 먼저 조회, 없으면 데이터베이스에서 조회
  • 영속성 컨텍스트에 해당 엔티티 존재 시 메모리에서 바로 조회(1차 캐시)
//최초 조회, 데이터베이스에서 조회
Member member1 = em.find(Member.class, 1L);
//두 번째 조회, 영속성 컨텍스트에 존재하므로 데이터베이스 조회 x
Member member2 = em.find(Member.class, 1L);

//member1 == member2 → 주소 값이 같은 인스턴스

📝 JPQL

  • 항상 데이터베이스에 SQL 실행하여 결과 조회
  • 데이터베이스를 먼저 조회
/*첫 번째 호출: 데이터베이스에서 조회*/
//데이터베이스에서 회원 엔티티 조회 후 영속성 컨텍스트에 등록
Member member 1 =
    em.createQuery("select m from Member m where m.id = :id", Member.class)
      .setParameter("id", 1L)
      .getSingleResult();
      
/*두 번째 호출: 데이터베이스에서 조회*/
//데이터베이스에서 같은 회원 엔티티 조회, 이미 조회한 동일한 엔티티 존재
//→ 새로 검색한 엔티티 버리고 영속성 컨텍스트에 존재하는 기존 엔티티 반환
Member member2 =
    em.createQuery("select m from Member m where m.id = :id", Member.class)
      .setParameter("id", 1L)
      .getSingleResult();
      
//member1 == member2 → 주소 값이 같은 인스턴스

📊 쿼리 결과가 영속성 컨텍스트에 존재


📖 JPQL의 특징

  • JPQL은 항상 데이터베이스 조회
  • JPQL로 조회한 엔티티는 영속 상태
  • 영속성 컨텍스트에 엔티티가 이미 존재시 기존 엔티티 반환


10.5.3 JPQL과 플러시 모드

✅ 쿼리와 플러시 모드

  • JPQL은 영속성 컨텍스트에 존재하는 데이터를 고려하지 않고 데이터베이스에서 데이터 조회
    → JPQL 실행 전 영속성 컨텍스트의 내용을 데이터베이스에 반영해야 함

📊 영속성 컨텍스트가 아직 플러시되지 않은 상황

💻 쿼리와 플러시 모드 예제

//가격을 1000→2000원으로 변경
//데이터베이스에는 1000원인 상태
product.setPrice(2000);

//가격이 2000원인 상품 조회
Product product2 =
    em.createQuery("select p from Product p where p.price = 2000")
      .getSingleResult();
  • 플러시 모드는 AUTO가 기본
    → 따로 설정하지 않으면 쿼리 실행 직전에 영속성 컨텍스트가 플러시 됨

💻 플러시 모드 설정

em.setFlushMode(FlushModeType.COMMIT);  //커밋 시에만 플러시

//가격을 1000→2000원으로 변경
product.setPrice(2000);

//1. em.flush() 직접 호출

//가격이 2000원인 상품 조회
Product product2 =
    em.createQuery("select p from Product p where p.price = 2000", Product.class)
      .setFlushMode(FlushModeType.AUTO)  //2. setFlushMode() 설정
      .getSingleResult();
  • COMMIT
    • 쿼리 실행 시 플러시 자동 호출 x
    • 쿼리 실행 전 플러시 호출하고 싶을 경우
      1. em.flush()를 통해 수동으로 플러시
      2. setFlushMode()로 해당 쿼리에서만 사용할 플러시 모드를 AUTO로 변경

        쿼리에 설정하는 플러시 모드는 엔티티 매니저에 설정하는 플러시 모드보다 우선권을 가짐


✅ 플러시 모드와 최적화

em.setFlushMode(FlushModeType.COMMIT)
  • FlushModeType.COMMIT

    • 트랜잭션을 커밋할 때만 플러시
      → JPA 쿼리 사용 시 영속성 컨텍스트에는 있지만 데이터베이스에 반영하지 않은 데이터 조회 불가능
      ∴ 데이터 무결성에 심각한 피해를 줄 수 있음

    • 플러시가 자주 발생할 때 사용하면 쿼리시 발생하는 플러시 횟수를 줄여 성능 최적화할 수 있음

      • FlushModeType.AUTO : 쿼리+커밋 → 총 4번 플러시
      • FlushModeType.COMMIT : 커밋 시에만 1번 플러시

📖 참고

JDBC를 직접 사용하여 SQL을 실행할 때 플러시 모드 고민 필요

  • JDBC로 쿼리 직접 실행 시 JPA는 JDBC가 실행한 쿼리를 인식할 방법 x
    → 별도의 JDBC 호출은 플러시 모드를 AUTO로 설정해도 플러시 발생 x
  • JDBC로 쿼리를 실행하기 직전에 em.flush()를 호출하여 영속성 컨텍스트의 내용을 데이터베이스에 동기화하는 것이 안전
profile
기쁘게 코딩하고 싶은 백엔드 개발자

0개의 댓글