JPA는 SQL을 추상화한 객체 지향 쿼리 언어 JPQL을 제공, 객체를 대상으로 쿼리를 작성하며 특정 DB에 의존하지 않는다.

 

// Member m 별칭 사용 필수
String jpql = "select m From Member m where m.name like '%hello%'";

List<Member> result = em.createQuery(jpql, Member.class).getResultList();
//getResultList() : 항상 리스트 반환, 결과가 없으면 빈 리스트 반환
//getSingleResult() : 단일 객체 반환, 결과가 없거나 둘 이상이면 Exception 발생


//TypeQuery : 반환 타입이 명확할 때
TypedQeury<Member> query = em.createQuery("SELECT m From Member m", Member.class);

//Query : 반환 타입이 명확하지 않을 때
Query query = em.createQuery("SELECT m.username, m.age from Member m");
// 파라미터 바인딩
String jqpl = selct m from Member m where m.username=:username;
em.createQuery(jpql, Member.class).setParameter("username", username);

// DTO로 바로 조회 (패키지명 포함한 전체 클래스명)
// 순서와 타입이 일치하는 생성자 필요
SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m

 

JPA는 where, having절에서만 서브 쿼리가 가능하고 from절에서는 서브 쿼리가 불가능하기 때문에 이 경우에는 join으로 풀어서 해결하거나 네이티브 SQL를 사용해서 해결한다. 어려운 경우 쿼리를 분해해서 2번 날리거나 서브 쿼리로 필터가 어느정도 된 결과를 애플리케이션에서 추가로 조작하는 등의 방법도 있다.

 

join을 직접 사용하지 않아도 연관 필드의 객체 그래프를 탐색하면 (m.team) 묵시적 내부 조인이 발생하는데 FROM절에 영향을 줄 수 있고 상황 파악이 어렵기 때문에 명시적으로 조인을 선언해주는 것이 좋다.

 

 


 

페치 조인 (join fetch),  DISTINCT

 

 

JPQL에서 제공하는 페치 조인을 사용하면 조인 시 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회할 수 있다. 글로벌 패치 전략을 지연 로딩으로 사용하는 경우 연관된 엔티티나 컬렉션은 사용 시점에 조회를 하는데 이때 연관 필드를 for문으로 탐색하게 되면 N+1 쿼리 문제가 발생하게 된다. 하지만 페치 조인을 사용하면 지연 로딩을 적용하지 않고 즉시 조회로 연관 엔티티의 값까지 전부 가져올 수 있다.

(일반 조인: SELECT T.* / 페치 조인: SELECT T.*, M.*)

 

String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();

for(Team team : teams) {
    System.out.println("teamname = " + team.getName() + ", team = " + team);
    for (Member member : team.getMembers()) {
        //페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
        //그냥 조인의 경우 member.getUsername()으로 접근 시 멤버 조회 쿼리 발생 (N+1)
        System.out.println(“-> username = " + member.getUsername()+ ", member = " + member); }
}

 

페치 조인 대상에는 별칭을 가급적 사용하지 않는 것이 좋고, 둘 이상의 컬렉션은 페치 조인 할 수 없다.

일대일, 다대일 같은 단일 값 연관 필드가 아닌 대다 컬렉션을 페치 조인하고 페이징 API를 사용하면 하이버네이트는 경고 로그를 남기고 메모리에서 페이징을 하기 때문에 매우 위험하다.

 

 객체 그래프의 사상은 연관된 데이터를 전부 불러오는 것인데 일대다 조인은 데이터가 늘어나기 때문에 페이징을 쓰면 중간에 짤리게 된다. 그래서 일대다 조인의 경우에는 hibernate.default_batch_fetch_size 글로벌 설정으로 BatchSize를 설정하면 (애노테이션도 있지만 글로벌 설정 권장) 설정 size만큼(default 50) select * from x where in (?, ?, ? ...) 이런식으로 조회

 

그래도 페치 조인은 성능 최적화에 매우 중요하기 때문에 글로벌 페치 전략은 지연 로딩으로, 최적화가 필요한 곳은 페치 조인을 적용하는 것이 좋다. (만약 여러 테이블을 join 해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면 페치 조인 대신 기본 조인을 하고 필요한 데이터만 조회해서 DTO로 반환하는 것이 좋다.)

 

추가로 JPQL에서는 join을 할 때 DISTINCT를 사용하면 특별한 기능이 추가 되는데 특정 팀에 해당하는 멤버를 join 할 때, 테이블 관점에서 보면 특정 팀에 해당하는 멤버 수만큼 결과 row가 생기는 것이 맞지만 JPA의 객체 관점에서 보면 팀 객체가 중복되어서 리스트로 반환되는 것이다. 그래서 JPQL은 DISTINCT 사용 시 중복 엔티티를 제거해준다. 

 

 


 

기타

 

JPQL 다형성 쿼리

//JPQL type()
select i from Item i where type(i) IN (Book, Movie)

//SQL
select i from Item i where i.DTYPE in (‘B’, ‘M’)

//JPQL treat()
select i from Item i where treat(i as Book).auther = ‘kim’

//SQL
select i.* from Item i where i.DTYPE = ‘B’ and i.auther = ‘kim’

 

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값(연관 엔티티는 외래 값)을 사용한다.

 

Named 쿼리를 사용하면 JPQL을 애노테이션, XML에 정의해두고 애플리케이션 로딩 시점에 쿼리를 검증해서 정적 쿼리로 사용할 수 있다.

@Entity
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {

}

List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
    .setParameter("username", "회원1")
    .getResultList();

 

벌크 연산

 

한번에 너무 많은 데이터를 변경하는 경우 변경 감지 기능은 너무 많은 SQL을 생성하는데 벌크 연산을 사용하면 변경 감지 대신 쿼리 한번으로 많은 데이터를 변경할 수 있다. 벌크 연산은 먼저 flush()를 한 다음 영속성 컨텍스트가 아닌 데이터베이스에 직접 쿼리를 전송하는데, 이때 영속성 컨텍스트에 있는 엔티티들은 벌크 연산의 처리 결과가 적용이 안 되어있는 상태다. 그래서 데이터 정합성에 문제 위험이 있기 때문에 1)벌크 연산 후에는 em.clear()로 영속성 컨텍스트 초기화를 해주거나 2)벌크 연산을 먼저 수행하고 조회를 하는 식으로 사용하는 것이 좋다.

 

String qlString = "update Product p " + 
                  "set p.price = p.price * 1.1 " +
                  "where p.stockAmount < :stockAmount";
                  
// executeUpdate() 결과는 변경 된 엔티티 수 반환
// UPDATE, DELETE 지원
int resultCount = em.createQuery(qlString)
                    .setParameter("stockAmount", 10)
                    .executeUpdate();

 

 

 

[참고] 인프런 김영한님 강의를 공부한 내용입니다.

+ Recent posts