(5) 객체지향 쿼리 심화
10.5.1 벌크 연산
엔티티를 수정하려면 영속성 컨텍스트의 변경 감지 기능이나 병합을 사용하고, 삭제하려면 EntityManager.remove() 메소드를 사용
하지만 이 방법으로는 수백개 이상의 엔티티를 하나씩 처리하기에는 시간이 너무 오래 걸린다.
이 때, 여러 건을 한 번에 수정하거나 삭제하는 벌크 연산을 사용하면 된다! executeUpdate()
메서드 사용
UPDATE 벌크 연산
벌크 연산은 executeUpdate()
메소드를 사용한다. 해당 메소드는 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다.
String qlString ="update Product p " +"set p.price = p.price * 1.1 " +"where p.stockAmount < :stockAmount"; // 재고가 10개 미만인 모든 상품의 가격을 10% 상승int resultCount = em.createQuery(qlString) // 벌크 연산으로 영향을 받은 엔티티 건수를 반환.setParameter("stockAmount", 10).executeUpdate();
DELETE 벌크 연산
String qlString ="delete from Product p " +"where p.price < :price";int resultCount = em.createQuery(qlString) // 벌크 연산으로 영향을 받은 엔티티 건수를 반환.setParameter("price", 10) // 가격이 100원 미만인 상품을 삭제하는 코드.executeUpdate();
JPA 표준은 아니지만 하이버네이트는 INSERT 벌크 연산도 지원한다.
🚨 주의사항
벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리한다.
벌크 연산 주의사항 예제
// 상품 A 조회(상품A의 가격은 1000원이다.)Product productA = em.createQuery("select p from Product p where p.name = :name",Product.class).setParameter("name", "productA").getSingleResult();// 출력 결과: 1000System.out.println("productA 수정 전 = " + productA.getPrice());// 벌크 연산 수행으로 모든 상품 가격 10% 상승em.createQuery("update Product p set p.price=price*1.1").executeUpdate();// 출력 결과: 1000System.out.println("productA 수정 후 = " + productA.getPrice());
상품 조회를 통해 가져온 Product 객체 productA가 있다. 이후, 벌크 연산 수행으로 객체의 정보를 수정했을 때, 영속성 컨텍스트를 지나치지 않고 DB의 정보를 바로 변경한다.
하지만 영속성 컨텍스트에 저장되어 있는 productA의 정보는 변경되지 않고 그대로 남아있는다. (1100원이 아니라 1000원 출력)
DB 에는 1100원으로 저장되어 있지만, 영속 컨텍스트에는 1000원으로 저장되어 있다. => 불일치
🚑 해결 방법
1. em.refresh()
사용
- 벌크 연산을 수행한 직후 em.refresh()를 사용해 DB에서 productA 를 다시 조회하면 된다.
ex)em.refresh(productA)
2. 벌크 연산 먼저 실행
가장 실용적인 해결책, 벌크 연산을 먼저 실행하고 나서 productA를 조회하면 된다.
변경된 사항을 영속 컨텍스트에도 저장함.
3. 벌크 연산 수행 후 영속성 컨텍스트 초기화
- 그렇게 되면 이후 엔티티를 조회할 때 벌크 연산이 적용된 DB에서 엔티티를 조회한다.
10.5.2 영속성 컨텍스트와 JPQL
쿼리 후 영속 상태인 것과 아닌 것
JPQL로 엔티티를 조회하면 영속성 컨텍스트에서 관리되지만 엔티티가 아니면 영속성 컨텍스트에서 관리되지 않는다. (임베디드 타입, 값 타입은 관리 X)
조회한 엔티티만 영속성 컨텍스트가 관리한다.
JPQL로 조회한 엔티티와 영속성 컨텍스트
만약, 영속성 컨텍스트에 회원1이 이미 있는데, JPQL로 회원 1을 다시 조회하면 조회한 결과를 버리고 대신에 영속성 컨텍스트에 있던 엔티티를 반환한다.
새로 조회한 엔티티로 대체한다면, 영속성 컨텍스트에 수정 중인 데이터가 사라질 수 있으므로 위험하기 때문이다.
영속성 컨텍스트는 영속 상태인 엔티티의 동일성을 보장한다.
em.find()
또는JPQL
을 사용하면 영속성 컨텍스트가 같으면 동일한 엔티티를 반환한다.이때
식별자 값
을 사용해서 비교한다.
find() vs JPQL
둘 다 주소 값이 같은 인스턴스를 반환하고 결과도 같다.
하지만 엔티티를 조회하는 순서가 다르다.
em.find() : 영속성 컨텍스트 -> DB
JPQL : DB -> 영속성 컨텍스트
1. em.find()
- 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 DB에서 찾는다. 따라서 해당 엔티티가 영속성 컨텍스트에 있으면 메모리에서 바로 찾으므로 성능상 이점 ! =>
1차 캐시
라고 부른다
//최초 조회, 데이터베이스에서 조회Member member1 = em.find(Member.class, 1L);//두번째 조회, 영속성 컨텍스트에 있으므로 데이터베이스를 조회하지 않음Member member2 = em.find(Member.class, 1L);// member1 == member2 는 주소 값이 같은 인스턴스
2. JPQL
em.find()
를 두 번 사용한 로직과 마찬가지로 주소 값이 같은 인스턴스를 반환하고 결과도 같다. 하지만 항상 데이터베이스에 SQL을 실행해서 결과를 조회한다.
- 처음 JPQL 호출 시 DB에서 엔티티를 조회하고 영속성 컨텍스트에 등록, 두 번째 JPQL 을 호출하면 DB에서 엔티티를 조회, 이미 영속성 컨텍스트에 조회한 같은 엔티티가 있으면 새로 검색한 엔티티를 버리고 영속성 컨텍스트에 있는 기존 엔티티를 반환한다.
- 특징
JPQL은 항상 데이터베이스를 조회한다.
JPQL로 조회한 엔티티는 영속 상태다.
영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환한다.
// 첫 번째 호출: 데이터베이스에서 조회Member member1 = em.createQuery("select m from Member m where m.id = :id", Member.class).setParameter("id", 1L).getSingResult();// 두 번째 호출: 데이터베이스에서 조회Member member2 = em.createQuery("select m from Member m where m.id = :id", Member.class).setParameter("id", 1L).getSingResult();// member1 == member2 는 주소값이 같은 인스턴스
10.5.3 JPQL과 플러시 모드
플러시
영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화하는 것이다.
JPA는 플러시가 일어날 때, 영속성 컨텍스트에 등록, 수정, 삭제한 엔티티를 찾아 INSERT, UPDATE, DELETE SQL을 만들어 DB에 반영한다. em.flush()
플러시 모드에 따라 커밋하기 직전이나 쿼리 실행 직전에 자동으로 플러시 호출 가능.
em.setFlushMode(FlushModeType.AUTO)
- 커밋 또는 쿼리 실행 시(실행 직전에) 플러시(기본값)
em.setFlushMode(FlushModeType.COMMIT)
커밋 시에만 플러시
성능 최적화를 위해 꼭 필요할 때만 사용해야 한다.
JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 DB에서 데이터를 조회. 따라서 JPQL 실행 전 영속성 컨텍스트의 내용을 DB에 반영(플러시)해야 한다.
=> 데이터 무결성
그렇지 않으면, 영속성 컨텍스트의 엔티티 정보는 변경되었지만 DB 에는 변경사항이 적용되지 않는 문제가 발생할 수 있다.
// 플러시 모드 설정em.setFlushMode(FlushModeType.COMMIT);product.setPrice(2000);em.flush(); //1. 직접 호출Product product2 =em.createQuery("select p from Product P where p.price = 2000", Product.class).setFlushMode(FlushModeType.AUTO) // 2. setFlushMode() 설정.getSingleResult();
플러시 모드를 COMMIT 으로 설정해놓아서 자동으로 플러시를 호출하지 않는다.
1번처럼 수동으로 플러시를 하거나
2번처럼 해당 쿼리에서만 AUTO 모드로 플러쉬모드를 적용하면 된다.
🤔 COMMIT 모드를 사용하는 이유
플러시가 너무 자주 일어나는 상황에 이 모드를 사용하면 쿼리할 때 발생하는 플러시 횟수를 줄여서 성능을 최적화할 수 있다.
예를 들면, 등록(), 쿼리() 가 여러 번 반복되는 경우
- 만약 JPA를 거치지 않고 JDBC로 쿼리를 실행한다면 JPA는 JDBC가 실행한 쿼리를 인식할 수 없다. 따라서 AUTO 모드를 해도 플러시가 일어나지 않는다
- 이 때는 JDBC로 쿼리를 실행하기 전에
em.flush()
를 호출해서 영속성 컨텍스트의 내용을 데이터베이스에 동기화해주는 것이 안전하다.
정리
- JPQL은 SQL을 추상화해서 특정 데이터베이스 기술에 의존하지 않는다.
- QueryDSL은 JPQL을 만들어주는 빌더 역할. 핵심은 JPQL
- QueryDSL을 사용하면 동적 쿼리를 편리하게 작성할 수 있다. + 직관적이고 편리함
- JPA도 네이티브 SQL을 제공하므로 직접 SQL 사용 가능하다. 하지만 특정 DB에 종속적인 SQL 사용 시 다른 DB로 변경이 쉽지 않다.
- 최대한 JPQL을 사용하고, 그래도 방법이 없다면 네이티브 SQL을 사용
- JPQL은 대용량 데이터 수정/삭제를 위해 벌크 연산을 지원한다.