JPQL 중급 - 다양한 사용법 및 주의점

식빵·2022년 1월 21일
0

JPA 이론 및 실습

목록 보기
14/17

🍀 JPQL의 다양한 사용법

👏 다형성 쿼리

이전에 작성한 글의 엔티티 코드를 그대로 사용해서 다형성 쿼리 테스트 코드를 작성했다.

참고: @Inheritance(strategy = InheritanceType.JOINED) 를 사용 중이다.


테스트 코드

String queryString = "select e from Employee e where type(e) " 
                      + "in (HourlyEmployee, SalariedEmployee )";
List<Employee> list1 = em.createQuery(queryString, Employee.class).getResultList();
for (Employee employee : list1) {
    System.out.println("employee = " + employee.getClass().getSimpleName());
}


String queryString = "select e from Employee e " 
                  +"where treat(e as SalariedEmployee).annualSalary = '1023'";
List<Employee> list2 = em.createQuery(queryString, Employee.class).getResultList();
for (Employee employee : list2) {
    System.out.println("employee = " + employee.getClass().getSimpleName());
}




👏 엔티티 직접 사용


엔티티 직접사용 - 기본키


테스트 코드

Developer devRef = em.getReference(Developer.class, 3L);

TypedQuery<Developer> query 
 = em.createQuery("select d from Developer d where d = :dev", Developer.class);
 
Developer dev = query.setParameter("dev", devRef).getSingleResult();

System.out.println("dev = " + dev);

/*
// 이 방법도 가능!
TypedQuery<Developer> query 
 = em.createQuery("select d from Developer d where d.id = :dev", Developer.class);
Developer dev = query.setParameter("dev", 3L).getSingleResult();
System.out.println("dev = " + dev);
*/

콘솔 출력

Hibernate: 
    select
        developer0_.developer_id as develope1_1_,
        developer0_.age as age2_1_,
        developer0_.company_id as company_4_1_,
        developer0_.name as name3_1_ 
    from
        developer developer0_ 
    where
        developer0_.developer_id=?
        
dev = Developer(id=3, name=naverDev1, age=26)



엔티티 직접사용 - 외래키


테스트 코드

String sql = "select d from Developer d where d.company = :comp";

Company ref = em.getReference(Company.class, 1L);
TypedQuery<Developer> query = em.createQuery(sql, Developer.class);
List<Developer> devList = query.setParameter("comp", ref).getResultList();

devList.forEach(System.out::println);

콘솔 출력

Hibernate: 
    select
        developer0_.developer_id as develope1_1_,
        developer0_.age as age2_1_,
        developer0_.company_id as company_4_1_,
        developer0_.name as name3_1_ 
    from
        developer developer0_ 
    where
        developer0_.company_id=?
Developer(id=3, name=naverDev1, age=26)
Developer(id=4, name=naverDev2, age=22)




👏 NamedQuery

하나의 쿼리에 이름을 지정하는 것이다.

특징은 아래와 같다.

  • 미리 정의해서 이름을 부여한 JPQL
  • 정적 쿼리
  • 애플리케이션 로딩 시점에 초기화 및 재사용
  • 애플리케이션 로딩 시점에 쿼리 검증

테스트를 해보자.


- 엔티티 코드 수정

@NamedQuery(
        name = "Developer.findByName",
        query = "select d from Developer d where d.name = :name"
)
public class Developer {...}

- 테스트 코드

List<Developer> developers
        = em.createNamedQuery("Developer.findByName", Developer.class)
        .setParameter("name", "naverDev1")
        .getResultList();

for (Developer developer : developers) {
    System.out.println("developer = " + developer);
}

- 콘솔 출력

Hibernate: 
    select
        developer0_.developer_id as develope1_1_,
        developer0_.age as age2_1_,
        developer0_.company_id as company_4_1_,
        developer0_.name as name3_1_ 
    from
        developer developer0_ 
    where
        developer0_.name=?
developer = Developer(id=3, name=naverDev1, age=26)

그런데 만약에 내가 고의적으로 쿼리를 망가뜨리고 실행하면? 애플리케이션이 뜨면서 부터
에러를 잡아낸다. 덕분에 배포하기 전에 빠르게 버그를 발견하고 수정할 수 있다.


- @NamedQuery : query 속성 변경
query = "select d from Developer_WHAT d where d.name = :name"


- 에러!


참고: 스피링 JPA 의 @Query 도 같은 기능을 제공한다,
단지 name을 지정 안하는 차이만 있다. 그래서 이름없는 NamedQuery라고도 한다.




👏 벌크 연산

우리는 쿼리를 직접 사용해봤다면 update(또는 delete)문을 사용할 때,
where 절로 필터링하여 여러 건을 한번에 변경시키는 경우가 많다.

그런데 JPA 의 변경 감지 기능만 사용하면 많은 SQL 을 실행하게 된다.
이런 이유로 JPQL은 벌크 연산을 제공한다.

테스트 코드를 작성해보자.
좀 이상한 테스트지만, 모든 회사원의 나이를 20살로 바꾸겠다.


- 테스트 코드

int cnt = em.createQuery("update Developer d set d.age = 20")
        .executeUpdate();

System.out.println("cnt = " + cnt);

- 콘솔 출력

Hibernate: 
    update
        developer 
    set
        age=20
cnt = 4

- 테이블 데이터 확인


executeUpdate() 의 결과는 변형이 일어난 엔티티의 수를 반환시킨다.

참고
JPA 에서는 UPDATE, DELETE 만 지원
Hibernate 에서는 INSERT(insert into ..select) 도 가능하다!




🚷 벌크 연산 주의점

벌크 연산은 영속성 컨텍스트의 상태와 상관없이 무족건 데이터베이스에 바로 쿼리를 날린다.
물론 벌크 연산도 JPQL이기 때문에 실행되기 전에 flush가 수행되서 이전까지의
SQL 저장소에 있던걸 먼저 날린 후에, 벌크 연산을 하기는 한다.

하지만 여전히 문제가 있다.

아래 코드를 보자.


테스트 코드

Developer developer = new Developer();
developer.setAge(25);
developer.setName("dev1");
developer.setCompany(null);
em.persist(developer);

int cnt = em.createQuery("update Developer d set d.age = 20")
        .executeUpdate();

System.out.println("age = " + developer.getAge());
System.out.println("cnt = " + cnt);

콘솔 출력

Hibernate: 
    insert 
    into
        developer
        (age, company_id, name, developer_id) 
    values
        (?, ?, ?, ?)
        
Hibernate: 
    update
        developer 
    set
        age=20
        
age = 25 // 영속성 컨텍스트의 엔티티는 여전히 그대로다..!
cnt = 5

이러면 이후에 코드를 짤 때 많은 혼동을 일으킬 수 있다.
이에 대한 대처법은 두 가지로 나뉜다.

  • 맨 마지막에 벌크 연산을 한다.
  • 벌크 연산 이후에 추가적인 코드가 있다면, em.clear()를 호출하여 영속성 컨텍스트의 엔티티 정보를 모두 지운다.




👏 EntityManager.find() VS JPQL 조회

다음과 같은 상황에서 좀 궁금한 게 생긴다.


테스트 코드

Developer findDev = em.find(Developer.class, 3L);
System.out.println("ref " + findDev.hashCode());

Developer jpqlDev = em.createQuery("select d from Developer d where d.id = 3L", Developer.class)
        .getSingleResult();

System.out.println("ref " + jpqlDev.hashCode());

System.out.println("is Same ? " + (findDev == jpqlDev));

em.find 를 통해서 얻어온 엔티티는 영속성 컨텍스트의 관리를 받게 된다.
이런 상태에서 JPQL 사용하면 일단은 쿼리가 무조건 날라간다.
그렇다면 em.find 로 가져온 엔티티는 없어지고 JPQL로 얻어온 엔티티를 새로이 사용할까?

콘솔 출력을 통해서 답을 알아보자.



콘솔 출력

Hibernate: 
    select
        developer0_.developer_id as develope1_1_0_,
        developer0_.age as age2_1_0_,
        developer0_.company_id as company_4_1_0_,
        developer0_.name as name3_1_0_ 
    from
        developer developer0_ 
    where
        developer0_.developer_id=?
        
ref 1779378259

Hibernate: 
    select
        developer0_.developer_id as develope1_1_,
        developer0_.age as age2_1_,
        developer0_.company_id as company_4_1_,
        developer0_.name as name3_1_ 
    from
        developer developer0_ 
    where
        developer0_.developer_id=3
        
ref 1779378259

is Same ? true

놀랍게도 em.find 로 찾아온 엔티티를 그대로 사용한다.

결론: JPQL로 쿼리를 날려서 가져온 엔티티가 이미 영속성 컨텍스트 내에 있다면,
JPQL로 조회한 결과를 버리고 이미 있는 것을 계속 쓴다
.

이러는 이유는 영속성 컨텍스트의 엔티티 동일성 보장 때문이다.




👏 JPQL 특징

  • JPQL은 항상 데이터베이스를 조회한다.
  • JPQL로 조회한 엔티티는 영속 상태다
  • 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면, 기존 엔티티를 반환한다.




🍀 참고

자바 ORM 표준 JPA 프로그래밍
인프런 JPA 관련 로드맵

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글