[JPA] JPQL - 추가 내용 (다형성 쿼리, Named 쿼리, 벌크 연산 등)

3Beom's 개발 블로그·2023년 6월 18일
0

SpringJPA

목록 보기
17/21

출처

본 글은 인프런의 김영한님 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 수강하며 기록한 필기 내용을 정리한 글입니다.

-> 인프런
-> 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의


다형성 쿼리

  • 만약 다음과 같이 DB 상에서 슈퍼-서브 테이블 구조로 설계했을 경우,

  • 조회 대상을 특정 자식으로 한정시킬 수 있다.
  • 예시
    • Item 중에 Book, Movie를 조회한다.

    • JPQL : SELECT i FROM Item i WHERE TYPE(i) IN (Book, Movie)

    • SQL : SELECT i.* FROM Item i WHERE i.DTYPE IN ('B', 'M')

      → 부모 테이블에서 자식 테이블 데이터 종류를 구분하기 위한 컬럼 DTYPEIN 구문을 적용해준다.

  • TREAT
    • 자바의 타입 캐스팅과 유사하다.
    • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.
    • FROM, WHERE 절에서 사용할 수 있으며, Hibernate 의 경우, SELECT 절에서도 사용할 수 있다.
    • JPQL : SELECT i FROM Item i WHERE TREAT(i as Book).author = 'kim'
    • SQL : SELECT i.* FROM Item i WHERE i.DTYPE = 'B' AND [i.author](http://i.author) = 'kim'

엔티티 직접 사용 - 기본 키 값

  • JPQL 에서 엔티티를 직접 사용하게 되면, SQL 에서 해당 엔티티의 기본 키 값을 사용하게 된다.
    • JPQL
      • SELECT COUNT(m.id) FROM Member m

      • SELECT COUNT(m) FROM Member m

        → 둘 모두 동일한 SQL이 실행된다.

    • SQL
      • SELECT COUNT(m.id) as cnt FROM Member m
  • 파라미터를 바인딩 할 때, 엔티티 자체를 직접 넘겨도 똑같이 SQL에서는 기본 키를 기준으로 동작한다.
    • JPQL
      String jpql = "SELECT m FROM Member m WHERE m = :member";
      List resultList = em.createQuery(jpql)
      											.setParameter("member", member)
      											.getResultList();
      → member 엔티티 자체를 전달
      String jpql = "SELECT m FROM Member m WHERE m.id = :memberId";
      List resultList = em.createQuery(jpql)
      											.setParameter("memberId", member.getId())
      											.getResultList();
      → member 기본키를 전달 ⇒ 두 방식 모두 동일한 SQL이 생성된다.
    • SQL SELECT m.* FROM Member m WHERE m.id=?

엔티티 직접 사용 - 외래 키 사용

  • 외래 키도 똑같이 연관관계 엔티티 자체를 파라미터로 전달할 경우, 해당 엔티티의 기본 키를 기준으로 SQL이 생성된다.
    • JPQL
      String jpql = "SELECT m FROM Member m WHERE m.team = :team";
      List resultList = em.createQuery(jpql)
      											.setParameter("team", teamA)
      											.getResultList();
      String jpql = "SELECT m FROM Member m WHERE m.team.id = :teamId";
      List resultList = em.createQuery(jpql)
      											.setParameter("teamId", team.getId())
      											.getResultList();
    • SQL SELECT m.* FROM Member m WHERE m.TEAM_ID=?

Named 쿼리

  • Named 쿼리 : 미리 정의해서 이름을 부여해두고 사용하는 JPQL
  • 예시 <Named 쿼리 등록>
    @Entity
    @NamedQuery(
    		name = "Member.findByUsername",
    		query = "SELECT m FROM Member m WHERE m.username = :username")
    public class Member {
    		...
    }
    <Named 쿼리 활용>
    List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
    															.setParameter("username", "회원1")
    															.getResultList();
  • 특정 쿼리에 이름을 부여해놓고 불러와서 쓸 수 있다. : 재사용이 가능하다.
  • 정적 쿼리이다.
    • 특정 파라미터를 부여할 수 없다는게 아니라, 특정 조건에 따라 쿼리를 수정할 수 없다는 의미이다.
    • 동적으로 특정 파라미터를 바인딩 할 수는 있다.
  • 어노테이션을 활용해 등록해 두거나, 혹은 XML에 정의해서 활용할 수 있다.
  • 애플리케이션 로딩 시점에 초기화 후 재사용한다.
    • 즉, JPQL → SQL 파싱 과정이 애플리케이션 로딩 시점에 이루어진다.
    • 해당 시점에 파싱한 후 캐싱해두고 호출될 때마다 가져다 쓴다.
    • JPQL → SQL 파싱에 소요되는 시간을 줄여준다.
  • 애플리케이션 로딩 시점에 쿼리를 검증해준다.
    • 매우 강한 강점이다.
    • 개발 과정에서 JPQL 오류를 컴파일 과정에서 확인해 볼 수 있는 것!
  • XML로 등록하는 방법은 다음과 같다.
    • META-INF/persistence.xml 파일의 <persistence-unit> 태그 내 다음 내용을 등록한다.
      <persistence-unit name="~~">
      	<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을 배포할 수 있다.
  • Spring Data JPA 를 활용할 경우, 다음과 같이 인터페이스 내 메서드에 @Query 어노테이션을 부여함으로써 바로 Named 쿼리를 생성할 수 있다.
    public interface UserRepository extends JpaRepository<User, Long> {
    	...
    
    	@Query("SELECT u FROM User u WHERE u.emailAddress = ?1")
    	User findByEmailAddress(String emailAddress);
    
    	...
    }
    → 이렇게 되면 애플리케이션 로딩 시점에 모든 오류를 잡아준다.

벌크 연산

  • 2개 이상의 데이터에 UPDATE, DELETE 문을 적용하는 것.
  • 예시 설정
    • 재고가 10개 미만인 모든 상품의 가격을 10% 상승한다.
  • JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL이 실행된다. (조회한 엔티티 값이 변경되면 자동으로 감지해서 update 문을 보내주는 기능)
    1. 재고가 10개 미만인 상품을 리스트로 조회
    2. 상품 엔티티 가격 10% 증가
    3. 트랜잭션 커밋 시점에 변경 감지가 동작함
  • 만약 변경된 엔티티가 100개라면, 100번의 UPDATE SQL이 실행된다.
  • 이를 보완하기 위한 기능이 벌크 연산이다.
  • 쿼리 한번으로 여러 테이블 ROW 를 변경한다.
  • PreparedStatement 로 executeUpdate() 했던 것처럼 똑같이 해당 메서드를 활용할 수 있다.
    String jpql = "UPDATE Product p SET p.price = p.price * 1.1 WHERE p.stockAmount < :stockAmount";
    
    int resultCount = em.createQuery(jpql)
    											.setParameter("stockAmount", 10)
    											.executeUpdate();
    → 영향을 받은 엔티티 수를 반환한다.
  • UPDATE, DELETE 를 지원한다.
  • Hibernate 의 경우, INSERT 도 지원한다. INSERT INTO .. SELECT 도 지원한다. (SELECT 한 결과를 삽입)

벌크 연산 주의

  • 벌크 연산을 활용할 경우, 다음 사항을 유의해야한다.

  • 벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리를 보내는 것이다.

  • 따라서 영속성 컨텍스트와 다음과 같은 충돌이 일어날 수 있다.

    • 다음과 같이 Member 데이터를 저장하는데, age는 설정하지 않고 persist 한다. : 0으로 설정된다.

      Member member1 = new Member();
      member1.setUsername("회원1");
      em.persist(member1);
      
      Member member2 = new Member();
      member2.setUsername("회원2");
      em.persist(member2);
      
      Member member3 = new Member();
      member3.setUsername("회원3");
      em.persist(member3);
    • 이후 바로 모든 Member의 age를 20으로 수정하는 벌크 연산을 수행한다.

      int resultCount = em.createQuery("update Member m set m.age = 20")
                          .executeUpdate();
    • 이렇게 되면 다음과 같이 DB에는 반영이 되어있지만,

    • 다음과 같이 영속성 컨텍스트에는 반영되어 있지 않게 된다.

      Member findMember1 = em.find(Member.class, member1.getId());
      Member findMember2 = em.find(Member.class, member2.getId());
      Member findMember3 = em.find(Member.class, member3.getId());
      
      System.out.println("member1 age : " + findMember1.getAge());
      System.out.println("member2 age : " + findMember2.getAge());
      System.out.println("member3 age : " + findMember3.getAge());

    • 벌크 연산도 JPQL 이기 때문에 JPQL 쿼리 날아가기 전에 flush() 되어 영속성 컨텍스트에 저장되고 DB에 반영된다. : 피할 수 없다.

  • 이와 같은 문제를 해결하기 위해서는 다음 두 가지 방안 중 하나에 따르면 된다.

    • 방법1 : 영속성 컨텍스트에 저장되기 이전에 먼저 벌크 연산을 실행한다.
      • 영속성 컨텍스트가 갱신되지 않아 발생하는 문제를 애초에 차단하는 것이다.
    • 방법2 : 이미 영속성 컨텍스트에 있을 경우, 벌크 연산을 실행한 후 바로 영속성 컨텍스트를 초기화한다.
      • 영속성 컨텍스트의 값은 최신값이 아니기 때문에 아예 영속성 컨텍스트를 초기화시키는 것이다. 그럼 후에 해당 데이터가 필요하면 다시 조회하게 된다.
  • 이에 따라 위 예시를 다음과 같이 수정해주어야 한다.

    Member member1 = new Member();
    member1.setUsername("회원1");
    em.persist(member1);
    
    Member member2 = new Member();
    member2.setUsername("회원2");
    em.persist(member2);
    
    Member member3 = new Member();
    member3.setUsername("회원3");
    em.persist(member3);
    
    int resultCount = em.createQuery("update Member m set m.age = 20")
                        .executeUpdate();
    
    em.clear(); // 영속성 컨텍스트를 초기화해주어야 한다.
    
    Member findMember1 = em.find(Member.class, member1.getId());
    Member findMember2 = em.find(Member.class, member2.getId());
    Member findMember3 = em.find(Member.class, member3.getId());
    
    System.out.println("member1 age : " + findMember1.getAge());
    System.out.println("member2 age : " + findMember2.getAge());
    System.out.println("member3 age : " + findMember3.getAge());

  • Spring Data JPA 에서는 해당 과정이 자동으로 이루어질 수 있다.

profile
경험과 기록으로 성장하기

0개의 댓글