객체지향 쿼리 심화
- 한 번에 여러 데이터를 수정할 수 있는 벌크 연산
- JPQL과 영속성 컨텍스트
- JPQL과 플러시 모드
벌크 연산
- 엔티티를 수정하려면 영속성 컨텍스트의 변경 감지 기능이나 병합을 사용하고, 삭제는 EntityManager.remove() 메소드 사용
하지만 이 방법으로는 수백 개 이상의 엔티티를 하나씩 처리하기에는 시간이 너무 오래 걸리므로
여러 건을 한 번에 수정하거나 삭제하는 벌크 연산을 사용
// 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(); // 벌크 연산은 executeUpdate() 메소드를 사용
// 벌크 연산으로 영향을 받은 엔티티 건수를 반환
// DELETE 벌크 연산
// 가격이 100원 미만인 상품을 삭제하기
String qlString =
"delete from Product p " +
"where p.price < :price";
int resultCount = em.createQuery(qlString)
.setParameter("price", 100)
.executeUpdate(); // 벌크 연산은 executeUpdate() 메소드를 사용
- 벌크 연산의 주의점
벌크 연산을 사용할 때는 벌크 연산이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리하므로
영속성 컨텍스트에 있는 것과 데이터베이스에 있는 것이 달라 문제가 발생할 수 있음
// 벌크 연산 시 주의점 예제
// 상품A 조회 (상품A의 가격은 1000원)
// 1. 가격이 1000원인 상품A를 조회했으며, 조회된 상품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% 상승
// 2. 벌크 연산으로 모든 상품의 가격을 10% 상승시켰으므로 1100원이 되어야 함
em.createQuery("update Product p set p.price = p.price * 1.1").executeUpdate();
// 출력 결과 : 1000
// 3. 벌크 연산을 수행한 후 상품A의 가격을 출력하면 기대했던 1100원이 아닌 1000원이 출력됨
/* 벌크 연산은 영속성 컨텍스트를 통하지 않고 데이터베이스에 직접 쿼리하므로
영속성 컨텍스트에 있는 상품A와 데이터베이스에 있는 상품A의 가격이 다르므로 일어난 문제 */
System.out.println("productA 수정 후 = " + productA.getPrice());
- 벌크 연산의 문제를 해결하는 방법
- em.refresh() 사용
벌크 연산을 수행 직후 정확한 상품A 엔티티를 사용해야 하면 em.refresh()를 사용해 데이터베이스에서 상품A를 다시 조회
예) em.resfresh(productA); - 벌크 연산 먼저 실행 (가장 실용적인 해결책)
벌크 연산을 먼저 실행하고 나서 상품A를 조회하면 벌크 연산으로 이미 변경된 상품A를 조회하게 됨
이 방법은 JPA와 JDBC를 함께 사용할 때도 유용 - 벌크 연산 수행 후 영속성 컨텍스트 초기화
벌크 연산을 수행한 직후에 바로 영속성 컨텍스트를 초기화해서 영속성 컨텍스트에 남아 있는 엔티티를 제거
이렇게 영속성 컨텍스트를 초기화하면 이후 엔티티를 조회할 때 벌크 연산이 적용된 데이터베이스에서 엔티티를 조회
- em.refresh() 사용
영속성 컨텍스트와 JPQL
- 쿼리 후 영속 상태인 것과 아닌 것
JPQL의 조회 대상은 엔티티, 임베디드 타입, 값 타입 같이 다양한 종류가 있으며,
JPQL로 엔티티를 조회하면 영속성 컨텍스트에서 관리되지만 엔티티가 아니면 영속성 컨텍스트에서 관리되지 않음
// 조회한 엔티티만 영속성 컨텍스트가 관리
// 엔티티를 조회하면 해당 엔티티가 가지고 있는 임베디드 타입은 함께 수정됨
select m from Member m // 엔티티 조회 (관리 O)
// 영속성 컨텍스트가 관리하지 않으므로 변경 감지에 의한 수정이 발생하지 않음
select o.address from Order o // 임베디드 타입 조회 (관리 X)
select m.id, m.username from Member m // 단순 필드 조회 (관리 X)
- JPQL로 조회한 엔티티와 영속성 컨텍스트
JPQL로 데이터베이스에서 조회한 엔티티가 영속성 컨텍스트에 이미 있을 경우,
1. 새로운 엔티티를 영속성 컨텍스트에 하나 더 추가할 경우, 같은 기본 키 값을 가진 엔티티를 등록할 수 없으며
2. 기존 엔티티를 새로 검색한 엔티티로 대체할 경우, 영속성 컨텍스트에서 수정 중인 데이터가 사라질 수 있어 동일성 보장 불가
그러므로 JPQL로 데이터베이스에서 조회한 결과를 버리고 대신 영속성 컨텍스트에 있던 엔티티를 반환하도록 하는 방법을 사용
이때 식별자 값을 사용해서 비교
1) JPQL을 사용해서 조회를 요청
2) JPQL은 SQL로 변환되어 데이터베이스를 조회
3) 조회한 결과와 영속성 컨텍스트를 비교
4) 식별자 값을 기준으로 member1은 이미 영속성컨텍스트에 있으므로 버리고 기존에 있던 member1이 반환 대상이 됨
5) 식별자 값을 기준으로 member2는 영속성 컨텍스트에 없으므로 영속성 컨텍스트에 추가
6) 쿼리 결과인 member1, member2를 반환하며, member1은 쿼리 결과가 아닌 영속성 컨텍스트에 있던 엔티티
// 만약 영속성 컨텍스트에 회원1이 이미 있는데, JPQL로 회원1을 다시 조회할 경우
em.find(Member.class, "member1"); // 회원1 조회
// 엔티티 쿼리 조회 결과가 회원1, 회원2
List<Member> resultList = em.createQuery("select m from Member m", Member.class).getResultList();
- find() vs JPQL
em.find() 메소드는 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾으므로
해당 엔티티가 영속성 컨텍스트에 있으면 메모리에서 바로 찾으므로 성능상 이점이 존재
그러므로 영속성 컨텍스트는 1차 캐시라고 불림
JPQL은 항상 먼저 데이터베이스에 SQL을 실행해서 결과를 조회
만약 영속성 컨텍스트에 조회한 엔티티가 존재한다면, 새로 검색한 엔티티는 버리고 영속성 컨텍스트에 있는 기존 엔티티 반환
// find()
// 최초 조회, 데이터베이스에서 조회
Member member1 = em.find(Member.class, 1L);
// 두 번째 조회, 영속성 컨텍스트에 있으므로 데이터베이스를 조회하지 않음
Member member2 = em.find(Member.class, 2L);
// member1 == member2는 주소 값이 같은 인스턴스
// JPQL
// 첫 번째 호출 : 데이터베이스에 조회
// 데이터베이스에서 회원 엔티티를 조회하고 영속성 컨텍스트에 등록
Member member1 = 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과 플러시 모드
- 플러시는 영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화하는 것으로
JPA는 플러시가 일어날 때 영속성 컨텍스트에 등록, 수정, 삭제한 엔티티를 찾아서 SQL을 만들어 데이터베이스에 반영
플러시를 호출하려면 em.flush() 메소드를 직접 사용해도 되지만
보통 플러시 모드에 따라 커밋하기 직전이나 쿼리 실행 직전에 자동응로 플러시가 호출됨
// 커밋 또는 쿼리 실행 시 플러시 (기본값으로 트랜잭션 커밋 직전이나 쿼리 실행 직전에 자동으로 플러시 호출)
em.setFlushMode(FlushModeType.AUTO);
// 커밋 시에만 플러시 (커밋 시에만 플러시를 호출하고 쿼리 실행 시에는 플러시를 호출하지 않음)
em.setFlushMode(FlushModeType.COMMIT);
- 쿼리와 플러시 모드
JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 데이터베이스에서 데이터를 조회하므로
JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해야 함
// 플러시 모드를 COMMIT으로 설정 전
// 가격을 1000 -> 2000원으로 변경
/* 영속성 컨텍스트의 상품 엔티티는 가격이 1000원에서 2000언으로 변경되지만
데이터베이스에는 1000원 성태로 남아있음 */
product.setPrice(2000);
// 가격이 2000원인 상품 조회
/* 플러시 모드를 따로 설정하지 않으면 플러시 모드가 AUTO이므로
쿼리 실행 직전에 영속성 컨텍스트가 플러시 되므로 2000원으로 수정한 상품을 조회할 수 있음 */
Product product2 = em.createQuery("select p from Product p where p.price = 2000", Product.class).getSingleResult();
// 플러시 모드를 COMMIT으로 설정 후
/* 플러시 모드를 COMMIT으로 설정했으므로 이는 커밋 시에만 플러시하도록 하므로
쿼리시에는 플러시 하지 않으므로 방금 수정한 데이터를 조회할 수 없음
그러므로 직접 em.flush()를 호출하거나
Query 객체에 플러시 모드를 설정해주어 해당 쿼리에서만 사용할 플러시 모드를 변경해줘야 조회 가능 */
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();
- 플러시 모드와 최적화
FlushModeType.COMMIT 모드는 트랜잭션을 커밋할 때만 플러시하고 쿼리를 실행할 때는 플러시하지 않으므로
잘못하면 데이터 무결성에 심각한 피해를 줄 수 있는지만
플러시가 너무 자주 일어나는 상황에서 이 모드를 사용하면 쿼리시 발생하는 플러시 횟수를 줄여서 성능을 최적화할 수 있음
// 비즈니스 로직
등록()
쿼리() // 플러시
등록()
쿼리() // 플러시
등록()
쿼리() // 플러시
커밋() // 플러시
/* 이 때,
AUTO 모드이면 쿼리와 커밋할 때 총 4번 플러시
COMMIT 모드이면 커밋 시에만 1번 플러시 */
- JDBC와 플러시 모드
JPA를 통하지 않고 JDBC로 쿼리를 직접 실행하면 JPA는 JDBC가 실행한 쿼리를 인식할 방법이 없으므로
별도의 JDBC 호출은 플러시 모드를 AUTO 설정해도 플러시가 일어나지 않으므로
JDBC로 쿼리를 실행하기 직전에 em.flush()를 호출해서 영속성 컨텍스트의 내용을 데이터베이스에 동기화하는 것이 안전
'Java-Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 웹 애플리케이션 제작 ② - 도메인 모델과 테이블 설계 (0) | 2022.05.12 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 웹 애플리케이션 제작 ① - 프로젝트 환경설정 (0) | 2022.05.12 |
[자바 ORM 표준 JPA 프로그래밍] 객체지향 쿼리 언어 ⑤ - 네이티브 SQL (0) | 2022.05.06 |
[자바 ORM 표준 JPA 프로그래밍] 객체지향 쿼리 언어 ④ - QueryDSL (0) | 2022.05.03 |
[자바 ORM 표준 JPA 프로그래밍] 객체지향 쿼리 언어 ③ - Criteria (0) | 2022.05.03 |