• JPQL : Java Persistance Query Language
  • JPA에서 복잡한 SELECT문을 수행할 때 모든 데이터를 엔티티 객체로 변환하여 검색하는 것은 불가능하다.
  • JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공한다.
  • 엔티티 객체를 대상으로 한 쿼리
  • SELECT절, WHERE, GROUP BY, HAVING, JOIN, ORDER BY, 집합함수 등을 지원
  • JPQL은 만들어진대로 단지 SQL로 변환만 할 뿐 최적화같은 것은 수행하지 않는다
    ex) SELECT m FROM Member m -> SELECT * FROM member

* 주의사항

  • FROM뒤에 엔티티 입력
  • JPQL 키워드는 대소문자 구분 X ex) SELECT = select
  • 엔티티와 속성은 대소문자 구분
  • 엔티티에 별명 필수

기본 문법

  • SELECT문
String inputName = "europani"

TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m WHERE m.name=:username", Member.class);

query.setParameter("username", inputName);
List<Member> resultList = query.getResultList();

# 체이닝
List<Member> resultList = em.createQuery("SELECT m FROM Member m WHERE m.name=:username", Member.class)
               .setParameter("username", inputName)
               .getResultList();

getResultList() : 결과가 1개 이상일 때, 리스트 반환 (결과가 없으면 빈 리스트 반환)
getSingleResult() : 결과가 1개 일 때, 단일 객체 반환 (결과가 없거나 2개 이상이면 Exception)

  • CASE식
SELECT
    CASE WHEN m.age <= 10 THEN '학생요금'
         WHEN m.age >= 60 THEN '경로요금'
         ELSE '일반요금'
    END
FROM Member m
  • 집합함수, 정렬
SELECT COUNT(m), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age) FROM Member m
  • 기본 함수
    • 문자 : CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE
    • 숫자 : ABS, SQRT, MOD
    • SIZE, INDEX

벌크 연산

  • 쿼리 한번으로 여러 ROW를 변경
  • executeUpdate() 사용 (벌크 연산된 결과수 return)
  • 벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리를 날린다
    • 데이터 유효성을 위해 벌크 연산 후 영속성 컨텍스트를 초기화 한 후 데이터를 사용해야 한다
int resultCount = em.createQuery("UPDATE Member m SET m.age = 20")
            .executeUpdate();
em.claer();     // 영속성 컨텍스트 초기화

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

Spring-data-jpa

  • @Query를 사용하여 JPQL을 입력할 수 있다.
@Repository
public interface MemberRepository extends JpaRepository<Member, Integer> {

     @Query("SELECT m FROM Member m")
     List<Member> selectAll();
}

LEFT JOIN

  • 스프링부트 2 버전 부터는 엔티티 클래스 내에 연관관계가 없어도 Left Join이 가능하다
@Entity
public class Member {
    @Id
    private String email;
    private String name;
}

@Entity
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long bno;
    private String title;
    private String content;

    @ManyToOne(fetch=FetchType.LAZY)
    private Member member;
}

@Entity
public class Reply {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long rno;
    private String text;
    private String replyer;

    @ManyToOne(fetch=FetchType.LAZY)
    private Board board;
}

→ Board에서 Member로 접근 가능(연관관계 O) / Member에서 Board로 접근 불가(연관관계 X)
→ Reply에서 Board로 접근 가능(연관관계 O) / Board에서 Reply로 접근 불가(연관관계 X)

1. 연관관계가 있는 경우

// ex) Board -> Member 접근
@Repository
public interface BoardRepository extends JpaRepository<Board, Long> {

     @Query("SELECT b, m FROM Board b LEFT JOIN b.member m WHERE b.bno=:bno")
     Object getBoardWithWriter(@Param("bno") Long bno);
}

2. 연관관계가 없는 경우

  • LEFT JOIN 할 엔티티를 직접 입력하고 ON 키워드를 사용하여 연결
// ex) Board -> Reply 접근
@Repository
public interface BoardRepository extends JpaRepository<Board, Long> {

     @Query("SELECT b, r FROM Board b LEFT JOIN Reply r ON r.board=b WHERE b.bno=:bno")
     Object getBoardWithReply(@Param("bno") Long bno);
}

N+1 문제

연관관계를 갖는 엔티티로 JPQL을 사용할 때 SQL 결과인 N번에 실행 SQL 1번이 더해진 N+1번의 SQL이 실행되는 문제

1. EAGER 로딩 전략 사용시

@Entity
public class Member {
    @Id
    private String id;
    private String username;

    @OneToMany(mappedBy="member", fetch=FetchType.EAGER)
    private List<Order> orders = new ArrayList<Order>)();
}

@Entity
class Order {
    @Id
    private Long id;

    @ManyToOne
    private Member member;
}
List<Orders> orders=em.createQuery("SELECT m FROM Member m", Member.class)
            .getResultList();
SELECT * FROM member                 // JPQL로 실행된 SQL
SELECT * FROM order WHERE member_id=?     // 지연로딩으로 실행된 SQL들
SELECT * FROM order WHERE member_id=?
SELECT * FROM order WHERE member_id=?
SELECT * FROM order WHERE member_id=?
...
  • JPQL로 SQL을 생성하여 실행한다. 그 결과를 엔티티에 담는다
  • 즉시로딩 전략이기에 order에 연관된 member를 영속성 컨텍스트에서 찾는데 없을 시 SQL을 실행한다
  • 그 결과 결과수 만큼의 SQL이 추가로 실행된다

2. LAZY 로딩 전략으로 데이터를 가져온 이후 가져온 데이터에서 하위 엔티티를 다시 조회하는 경우

@Entity
public class Member {
    ...
    @OneToMany(mappedBy="member", fetch=FetchType.LAZY)
    private List<Order> orders = new ArrayList<Order>)();
    ...
}
List<Orders> orders=em.createQuery("SELECT m FROM Member m", Member.class)
            .getResultList();
SELECT * FROM member
  • 이 경우에는 지연로딩으로 JPQL에서 회원만 조회해서 N+1 문제가 발생하지 않는다
  • 다만, 이후 비즈니스 로직에서 조회되지 않는 주문 데이터를 사용할 때 이를 조회하기 위해 N+1 문제가 발생한다
SELECT * FROM order WHERE member_id=?
SELECT * FROM order WHERE member_id=?
SELECT * FROM order WHERE member_id=?
SELECT * FROM order WHERE member_id=?
...

★ 해결방안 : FETCH 조인사용

  • 페치 조인을 사용하면 SQL 조인을 사용하여 연관된 엔티티를 함께 가져오기 때문에 N+1 문제가 발생하지 않는다
jpql = "SELECT m FROM Member m JOIN FETCH m.order"

SELECT m.*, o.* FROM member m INNER JOIN order o ON m.id=o.member_id

★ Fetch Join(페치 조인)

  • 기존 SQL의 조인 종류가 아니고 JPQL의 성능 튜닝을 위해 JPA에서 제공하는 조인이다
  • 연관된 엔티티 or 컬렉션을 SQL 한번에 함께 조회하는 기능
  • 페치 조인은 모든 컬럼을 가져온다 (프로젝션이 불가능하다)
  • INNER JOIN이 사용됨
  • 한계
    1. Pagination API를 사용할 수 없다
    2. 둘 이상의 컬렉션(@xxxToMany)은 fetch join를 사용할 수 없다


  • Member:Team = N:1(다대일) 양방향 관계를 갖는 두 엔티티
@Entity
public class Member {
    @Id
    private String id;
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="team_id")
    private Team team;
}

@Entity
public class Team {
    @Id
    private String id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
}
  • 페치조인 사용 : JOIN 뒤에 FETCH <조인컬럼> 입력한다
String jpql = "SELECT m FROM Member m JOIN FETCH m.team"

List<Member> members=em.createQuery(jpql, Member.class)
            .getResultList();
SELECT m.*, t.* FROM member m INNER JOIN team t ON m.team_id=t.id
  • 즉시로딩으로 조회한 것과 같은 결과가 나온다
  • 지연로딩을 사용했지만 페치조인을 사용했기 때문에 연관된 team 엔티티는 프록시가 아닌 실제 엔티티가 되어 지연로딩이 일어나지 않는다

  • cf) 일반조인
jpql = "SELECT m FROM Member m JOIN m.team"

SELECT m.* FROM member m INNER JOIN team t ON m.team_id=t.id
  • 일반조인의 SELECT문을 보면 팀만 조회되고 조인했던 회원은 전혀 조회되지 않았다

글로벌 로딩전략을 즉시 로딩으로 설정하면 애플리케이션의 성능에 영향을 미친다. 글로벌 로딩전략을 지연 로딩으로 설정하고 최적화가 필요하면 페치조인을 적용하는 것이 가장 베스트이다

컬렉션 페치조인 (1:N 관계)

  • 일대다(1:N) 방향으로 페치조인을 실행
jpql = "SELECT t FROM team t JOIN FETCH t.member"

SELECT t.*, m.* FROM team t INNER JOIN member m ON t.id=m.team_id
  • 다대일(N:1) 때와 똑같은 쿼리가 실행된다
  • 하지만, 중복된 결과를 가져오는 결과를 초래한다
    • Distinct로 중복을 제거
team = 팀A, members=2
team = 팀A, members=2
team = 팀B, members=1

image

Distinct

  • 일대다(1:N) 조인의 경우 DB의 row가 증가한다. 그 결과 같은 엔티티의 조회 갯수도 증가하게 된다.
  • JPQL에 DISTINCT를 추가하면 JPA는 중복된 엔티티를 1번만 가져온다
    • ex) team1에 속한 member1, member2가 조회 될 경우 team1은 한번만 가져와진다
String jpql = "SELECT DISTINCT t FROM team t JOIN FETCH t.member"
  • 하지만 DISTINCT를 사용한 컬렉션 페치조인으로 중복된 엔티티를 제거하면 페이징이 불가능하다
    • 일대다(1:N)에서 일(1)을 기준으로 페이징을 하는 것이 목적이지만 컬렉션 페치조인을 하면 데이터가 다(N)을 기준으로 row가 검색된다
    • 즉, Team을 기준으로 페이징을 하고 싶은데 다(N)인 Member가 기준이 된다

페이징 + 컬렉션

  • 한계를 돌파하기 위해서는 다음과 같은 방법을 사용한다
  • 요약 : 1:N 관계를 N:1 관계로 뒤집어서 페치조인한 후 페이징 API를 사용한다. 그리고 컬렉션에는 배치 사이즈를 지정해 설정한 갯수만 들고오게 한다
  1. xxxToOne 관계로 모두 페치조인
    • ToOne 관계는 row수를 증가시키지 않아서 영향이 없다
  2. 컬렉션은 페치조인이 아닌 지연 로딩으로 조회하고
  3. 배치사이즈 설정하여 지연 로딩 최적화
    • hibernate.default_batch_fetch_size : application.yml에서 글로벌 설정
    • @BatchSize : Entity의 해당 컬렉션 필드에 개별 설정

    • 이 옵션으로 컬렉션이나 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회한다
spring: 
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 1000

or

@Entity
public class Team {
    ...
    @OneToMany(mappedBy = "team")
    @BatchSize(size = 1000)
    private List<Member> members = new ArrayList<Member>();
}
  • 실행
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
    return em.createQuery(
        "select o from Order o" +
                " join fetch o.member m" +
                " join fetch o.delivery d", Order.class)
        .setFirstResult(offset)
        .setMaxResults(limit)
        .getResultList();
}
  • ToOne 관계를 갖는 Member와 Order를 페치조인 하고 페이징을 한다

결론 : 페치조인으로 쿼리수를 최적화하고 컬렉션을 최적화 하는데 페이징이 필요할 땐 배치사이즈를 설정하고, 필요없을 땐 컬렉션 페치조인을 사용한다

@EntityGraph

  • JPQL을 작성할 수 없을 때나 JpaRepository가 제공하는 기능에 페치조인을 적용하고 싶을 때 사용할 수 있다
  • 또 여러 개를 fetch join 시킬때 사용할 수 있다
@Repository
public interface MemberRepository extends JpaRepository<Member, Integer> {

    @EntityGraph(attributePaths = {"team"})
    @Query("SELECT m FROM Member m")
    Member findAllMembers();

    @EntityGraph(attributePaths = {"team"})
    Member findByUsername(String username);
}
SELECT m.*, t.* FROM member m LEFT OUTER JOIN team t ON m.team_id=t.id
  • OUTER JOIN이 사용됨
    • 카테시안 곱이 발생하여 중복된 결과가 출력된다
    • Distinct로 중복을 제거
@EntityGraph(attributePaths = {"team"})
@Query("SELECT DISTINCT m FROM Member m")
Member findAllMembers();