9osari.log
← 목록으로

[JPA] JPQL과 페치 조인

JPQL (Java Persistence Query Language)

JPQL란?

테이블이 아닌 객체를 검색하는 객체지향 쿼리

--SQL: 테이블 대상
SELECT * FROM MEMBER WHERE USERNAME = 'kim';

--JPQL: 엔티티 객체 대상
SELECT m FROM Member m WHERE m.username = 'kim';

단점

동적 쿼리를 만들기 어렵다.

// 동적 쿼리 만들기 어려움
String jpql = "select m from Member m where";
if (username != null) {
    jpql += " m.username = :username";
}
if (age != null) {
    jpql += " and m.age = :age";
}

객체지향 쿼리 언어 종류

JPQL

String jpql = "select m from Member m where m.username = :username";
List<Member> result = em.createQuery(jpql, Member.class)
    .setParameter("username", "kim")
    .getResultList();

Criteria

자바 코드로 JPQL을 작성할 수 있지만 복잡하고 실용성이 없음

QueryDSL - 오픈소스 라이브러리

자바 코드로 JPQL을 작성, 동적 쿼리 작성이 편리

// QueryDSL 사용 (간결하고 직관적!)
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember m = QMember.member;

List<Member> result = queryFactory
    .selectFrom(m)
    .where(m.username.eq("kim"))
    .fetch();

JDBC

JDBC 커넥션 직접 사용 또는 Spring JdbcTemplate, MyBatis 등과 함께 사용 가능

em.flush();  // 플러시로 DB 동기화
// 네이티브 SQL 실행

경로 표현식

. 을 찍어 그래프를 탐색

image.png

상태 필드 (State Field)

연관 필드 (Association Field)

단일 값 연관 필드

select m.team from Member m
// m.team은 단일 값 연관 필드
// 묵시적 조인 발생: SELECT * FROM MEMBER M INNER JOIN TEAM T ...

select m.team.name from Member m  // 계속 탐색 가능!

컬렉션 값 연관 필드

select t.members from Team t
// t.members는 컬렉션 값 연관 필드
// 묵시적 조인 발생

select t.members.username from Team t  // 오류 컬렉션에서 탐색 불가

컬렉션에서 탐색하려면 명시적 조인 사용

select m.username from Team t
join t.members m  // 명시적 조인으로 별칭 부여

명시적 조인 VS 묵시적 조인

명시적 조인

JOIN 키워드를 직접 사용

select m from Member m join m.team t

묵시적 조인

경로 표현식에 의해 묵시적으로 SQL 조인 발생(내부 조인만 가능)

select m.team from Member m  // team 접근 시 자동으로 조인 발생실무에서 묵시적 조인은 가급적 사용하지 말자 명시적 조인만 사용하자

실무 권장사항

묵시적 조인은 가급적 사용하지 말고, 명시적 조인만 사용하자

  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움
  • 성능 튜닝이 어려움
  • 명시적 조인을 사용하면 쿼리를 예측하기 쉬움

페치 조인 (Fetch Join)

JPQL에서 성능 최적화를 위해 제공하는 기능

연관된 엔티티나 컬렉션을 SQL 한번에 조회하는 기능

// 일반 조인
select m from Member m join m.team t

// 페치 조인
select m from Member m join fetch m.team

엔티티 페치 조인

회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)

String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
    .getResultList();

for (Member member : members) {
    // 페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 X
    System.out.println("username = " + member.getUsername() 
        + ", teamname = " + member.getTeam().getName());
}
SELECT M.*, T.* 
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID = T.ID

컬렉션 패치 조인

일대다 관계에서 컬렉션 페치 조인

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

for (Team team : teams) {
    System.out.println("team = " + team.getName() 
        + " | members = " + team.getMembers().size());
    
    for (Member member : team.getMembers()) {
        System.out.println("-> member = " + member);
    }
}

페치 조인과 DISTINT

JPQL의 DISTINCT는 2가지 기능 제공

String jpql = "select distinct t from Team t join fetch t.members";
List<Team> teams = em.createQuery(jpql, Team.class)
    .getResultList();
SELECT DISTINCT T.*, M.* 
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID

페치조인과 일반조인의 차이

일반조인

select t from Team t join t.members m
SELECT T.* FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID

페치 조인

select t from Team t join fetch t.members
SELECT T.*, M.* FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID

→ Team과 Member 모두 조회

페치조인의 한계

페치 조인 대상에는 별칭을 줄 수 없다.

--안됨
select t from Team t join fetch t.members m where m.age > 10

--이렇게 해야 함
select m from Member m where m.age > 10 and m.team = :team

둘 이상의 컬렉션은 페치 조인할 수 없다

--안됨
select t from Team t 
join fetch t.members 
join fetch t.orders

컬렉션을 패치조인하면 페이징 API를 사용할 수 없다

--경고 발생 (메모리에서 페이징)
String jpql = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(jpql, Team.class)
    .setFirstResult(0)
    .setMaxResults(10)
    .getResultList();

해결방법 →

--단일 값 연관 필드는 페이징 가능 (1:1, N:1)
String jpql = "select m from Member m join fetch m.team";
List<Member> result = em.createQuery(jpql, Member.class)
    .setFirstResult(0)
    .setMaxResults(10)
    .getResultList();

페치 조인 사용 전략

페치 조인 사용처

일반 조인 사용처

--DTO로 직접 조회
select new com.example.MemberTeamDto(m.username, t.name)
from Member m join m.team t

Named 쿼리 - 정적쿼리

미리 정의해서 이름을 부여해두고 사용하는 JPQL

@Entity
@NamedQuery(
    name = "Member.findByUsername",
    query = "select m from Member m where m.username = :username"
)
public class Member {
    ...
}
// 사용
List<Member> result = em.createNamedQuery("Member.findByUsername", Member.class)
    .setParameter("username", "kim")
    .getResultList();

장점→


벌크 연산

쿼리 한 번으로 여러 테이블 로우를 변경하는 연산

// 변경 감지(Dirty Checking)로 하면?
List<Member> members = em.createQuery("select m from Member m")
    .getResultList();

for (Member member : members) {
    member.setAge(member.getAge() + 1);  // 각각 UPDATE 쿼리
}
// 100명이면 100번의 UPDATE 쿼리 실행

UPDATE 벌크 연산

String qlString = "update Member m set m.age = m.age + 1";
int resultCount = em.createQuery(qlString)
    .executeUpdate();

System.out.println("resultCount = " + resultCount);
// 한 번의 쿼리로 모든 Member의 age 증가!

DELETE 벌크 연산

String qlString = "delete from Member m where m.age < 18";
int resultCount = em.createQuery(qlString)
    .executeUpdate();

벌크 연산 주의 사항

영속성 컨텍스트를 무시하고 DB에 직접 쿼리

Member member = new Member();
member.setUsername("kim");
member.setAge(20);
em.persist(member);

// 벌크 연산 실행
em.createQuery("update Member m set m.age = 30")
    .executeUpdate();

System.out.println(member.getAge());  // 20 (영속성 컨텍스트 값)
// DB에는 30, 영속성 컨텍스트에는 20 → 불일치

해결 방법

벌크연산을 먼저 실행

// 벌크 연산 먼저
em.createQuery("update Member m set m.age = 30")
    .executeUpdate();

// 그 다음 조회
Member member = em.find(Member.class, 1L);
System.out.println(member.getAge());  // 30

벌크연산 수행 후(flush()) 영속성 컨텍스트 초기화(clear)

Member member = em.find(Member.class, 1L);

// 벌크 연산
em.createQuery("update Member m set m.age = 30")
    .executeUpdate();

em.clear();  // 영속성 컨텍스트 초기화

Member findMember = em.find(Member.class, 1L);  // DB에서 다시 조회
System.out.println(findMember.getAge());  // 30

Spring Data JPA @Modifying

@Modifying(clearAutomatically = true)  // 자동으로 clear
@Query("update Member m set m.age = :age where m.id = :id")
int bulkAgeUpdate(@Param("age") int age, @Param("id") Long id);
···
← PREV [JPA] 연관관계 매핑 NEXT → [DevBid - HTTP] Form 환경에서 RESTful 설계