JPQL

Kevin·2023년 8월 15일
0

JPA(Hibernate)

목록 보기
6/9
post-thumbnail

어떤 방법을 사용하든 JPQL에서 모든 것이 시작된다.

✅JPQL의 특징

  • JPQL은 객체지향 쿼리언어다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
  • JPQL는 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
  • JPQL는 결국 SQL로 변환된다.

✅JPQL의 기본 문법

  • JPQL도 비슷하게, SELECT, UPDATE, DELETE 문을 사용할 수 있다.

  • 엔티티를 저장할 때는 EntityManager.persist() 메서드를 사용하면 되므로, INSERT 문은 없다.

    • DATA JPA에서는 save()를 호출하면 된다.
  • 엔티티와 속성은 대소문자를 구분하고, SELECT, FROM, AS와 같은 JPQL 키워드는 대소문자를 구분 하지 않는다.

  • JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다. 엔티티 명을 지정하지 않으면 클래스 명을 기본값으로 사용한다.

    • 엔티티 명이란 @Entity(name=”XXX”) 중 XXX이다.
  • 별칭은 필수이다. 별칭은 From 엔티티 AS 뒤에 지정할 수 있으며, AS는 생략 가능하다.

SELECT 문

SELECT m FROM Member As m where m.username = 'HELLO'
  • Member 엔티티 중 username 필드가 “HELLO”인 엔티티 반환

✅TypeQuery, Query

작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다.

반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 객체를 사용, 반환 타입을 명확하게 지정할 수 없으면 Query 객체를 사용하면 된다.

TypeQuery<Member> query = em.createQuery("select m from member m", Member.class);

em.createQuery()의 두 번째 파라미터에 반환할 타입을 지정하면, TypeQuery를 반환하고, 지정하지 않으면 Query를 반환한다.

query.getResultList() → 결과를 예제로 반환하며, 만약 결과가 없으면 빈 컬렉션을 반환한다.

query.getSingleResult() → 결과가 정확히 하나일 때 사용하며, 2개 이상이면 예외가 발생한다.

✅파라미터 바인딩

이름 기준 파라미터 바인딩

→ 파라미터를 이름으로 구분하는 방법이며, 앞에 :를 사용한다.

String usernameParam = "User1";

em.createQuery("select m from member m where m.username = :username", Member.class);

query.setParameter("username", usernameParam);

이름 기준 파라미터 바인딩 방식을 사용하는 것이 위치 기준 파라미터 방식 보다 더 명확하다.

파라미터 바인딩 방식이 아닌 직접적인 문자열을 넣는 방식은 SQL 인젝션 공격에 취약하다.

✅프로젝션

Select 절에 조회할 대상을 지정하는 것을 프로젝션이라고 한다.

  • 엔티티 프로젝션
    select m from Member m
    select m.team from member m
    • 원하는 객체를 바로 조회하는 것이다.
      • 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리한다.

  • 임베디드 타입 프로젝션
    select o.address from Order o // 가능
    select a from Address a // 불가능
    • 임베디드 타입은 절대 조회의 시작점이 될 수 없다.
      • 시작점 = From 뒤 엔티티
      • 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리하지 않는다.

  • 스칼라 타입 프로젝션
    List<String> usernames = em.createQuery("select username from member", String.class);
    • 숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라고 한다.

  • 여러 값 프로젝션
    Query query = em.createQuery("select m.username, m.age from member m");
    
    Query query = em.createQuery("select m.team, m.order from member m", String.class);
    • 스칼라 타입뿐만 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있고, 모두 영속성 컨텍스트에서 관리한다.

✅NEW 명령어

NEW 명령어를 사용하면, 객체 변환 작업을 쉽게 처리할 수 있다.

TypeQuery<UserDTO> query = em.createQuery("select new jpabook.jpql.userDTO(m.username, m.age)
													from Member m", UserDTO.class);
  • select 다음에 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있고, 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다.
    • 단 반드시 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
    • 순서와 타입이 일치하는 생성자가 필요하다.

Spring Data JPA에서는 이렇게 매핑하면 됩니다.

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
    private String id;
    private String name;
    private String phone;
    private String deptId;
    private String deptName;
}
public interface UserRepository extends JpaRepository<User, String> {
    @Query(value = "select " +
            "new io.velog.youmakemesmile.jpql.UserDto(user.id, user.name, user.phone, user.deptId, dept.name) " +
            "from User user " +
            "left outer join Dept dept on user.deptId = dept.id ")
    List<UserDto> findUserDept();
}

✅페이징 API

JPQL Paging

  1. 아래와 같이 JPQL @Query 구문에 parameter로 Pageable 인터페이스를 주면 된다.
//example1//in @Repository code@Query("select * from a_table order by a_table_column desc")
List<String> getStringValue(Pageable pageable);
//example2import org.springframework.data.domain.Pageable;

@Repository
public interface QuizStateRepository extends JpaRepository<QuizState, Integer> {

    @Query(value = "SELECT qs FROM QuizState qs join fetch qs.quiz join fetch qs.quizStateType WHERE qs.user = ?1 and qs.quizStateType.desc = 'NOT_SELECTED'")
    List<QuizState> findAllNotSelectedProblems(User user, Pageable pageable);

//...

✅집합과 정렬

집합은 집합함수와 함께 통계 정보를 구할 때 사용된다.

  • 집합 함수
    • COUNT → 결과 수를 구한다. 반환 타입 : Long
    • MAX, MIN → 최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용
    • AVG → 평균값을 구한다. 숫자 타입만 사용가능하고, 반환 타입 : Double
    • SUM → 합을 구한다.

Group By → 통계 데이터를 구할 때 특정 그룹끼리 묶어준다.

Having → Group By로 그룹화 한 통계 데이터를 기준으로 필터링 한다.

통계 쿼리는 보통 전체 데이터를 기준으로 처리하므로, 실시간으로 사용하기엔 부담이 많다. 결과가 아주 많다면 통계 결과만 저장하는 테이블을 별도로 만들어 두고, 사용자가 적은 새벽에 통계 쿼리를 실행해서 그 결과를 보관하는 것이 좋다.

정렬

Order By는 결과를 정렬할 때 사용한다.

select m from Member m order by m.age DESC, m.username ASC
  • ASC(오름차순)가 기본 값이다.

✅JPQL 조인

내부 조인

  • 내부 조인은 A, B 엔티티 중 공통적인 필드만 남는다.
select m from Member m inner join m.team t where t.name = :teamName;
  • inner는 생략할 수 있다.
  • teamName의 이름을 가진 Team의 Member를 찾는다.

JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다.

  • 연관 필드 → 다른 엔티티와 연관 관계를 가지기 위해 사용하는 필드를 의미한다.
  • JPQL은 join 명령어 다음에 조인할 객체의 연관 필드를 사용한다.

그러면 내부 조인을 사용하는게 성능 상 더 좋은 것 아닌가?

  • 왜냐하면 외부 조인은 A엔티티의 모든 필드를 다 가져오기에
  • GPT의 의견
    1. 내부 조인 (Inner Join):
      내부 조인은 두 테이블 사이에서 일치하는 행만 반환하는 조인 유형입니다. 즉, 조인 조건을 만족하는 데이터만 결과로 나타납니다. 내부 조인은 주로 데이터베이스 엔진에 의해 최적화되어 빠른 성능을 보이기 때문에, 대부분의 경우에서 내부 조인이 더 빠른 실행 속도를 가질 수 있습니다.

    2. 외부 조인 (Outer Join):
      외부 조인은 두 테이블 사이에서 일치하지 않는 행도 포함하여 결과를 반환합니다. 왼쪽 테이블의 모든 행을 포함하는 왼쪽 외부 조인과 오른쪽 테이블의 모든 행을 포함하는 오른쪽 외부 조인, 그리고 양쪽 테이블의 모든 행을 포함하는 전체 외부 조인 등이 있습니다. 외부 조인은 모든 데이터를 보존하면서 조인 결과를 생성할 수 있지만, 내부 조인에 비해 조금 더 복잡한 처리가 필요할 수 있어 실행 속도가 느릴 수 있습니다.

      그렇다면 왜 외부 조인을 사용할까요? 외부 조인은 다음과 같은 상황에서 유용합니다:

    • 누락된 데이터 확인: 외부 조인을 사용하면 어느 한쪽 테이블에만 있는 데이터도 확인할 수 있습니다. 이를 통해 데이터의 누락이나 오류를 쉽게 파악할 수 있습니다.

      결론적으로, 내부 조인은 대부분의 경우에 빠른 성능을 제공하지만, 외부 조인은 데이터의 누락을 확인하거나 보완적인 정보를 얻을 때 유용합니다. 데이터베이스 설계와 쿼리 작성 시에 상황에 맞게 적절한 조인 유형을 선택하는 것이 중요합니다.


외부 조인

  • 외부 조인은 A의 모든 열과 B의 공통 열을 조회한다.
select m from Member m left outer join m.team t
  • outer는 생략 가능하다.
  • Member의 모든 필드와 m.team과 일치하는 Team 엔티티를 조회한다.

컬렉션 조인

  • 일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라고 한다.

N → 1으로의 조인은 다대일 조인이면서, 단일 값 연관 필드를 사용한다. m.team

1 → N으로의 조인은 일대다 조인이면서, 컬렉션 값 연관 필드를 사용한다. m.members

select t, m from Team t left join t.members m

세타 조인

  • 조인에 참여하는 두 릴레이션의 속성 값을 비교하여, 조건을 만족하는 열만 반환

Join On 절

  • 조인 대상을 필터링 하고, 조인할 수 있다.
    • 외부 조인에서만 On을 사용하는데, 그 이유는 내부 조인에서는 Join에 where을 이미 부가 하기 때문에
select m, t from Member m left join m.team t on t.name = 'A'

✅Fetch 조인

  • SQL에 존재하는 조인의 종류는 아니지만, JPQL에서 성능 최적화를 위해 제공하는 기능이다.
  • 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이다.

엔티티 페치 조인

select m from Member m join fetch m.team
  • 이 경우에 Member와 Member와 연관된 Team 엔티티를 함께 조회한다.

  • 페치 조인은 별칭을 사용할 수 없다.

  • 연관된 엔티티 또한 함께 내부 조인을 하기에, 쿼리는 한번만 나간다!!!

    • 이 때 Member와 연관되어서 함께 조회된 Team 엔티티는 프록시 엔티티가 아니라 실제 엔티티이다.
      • 당연하지만, 실제로 Team 테이블에도 쿼리를 날렸기 때문에 가능한 것이다.
      • 그렇기에 연관된 Team 엔티티를 사용해도, 지연로딩이 일어나지 않는다.

**컬렉션 페치 조인**

select t from Team t join fetch t.members where t.name = '팀A'
  • 팀을 조회하면서, 페치 조인을 사용해서 연관된 회원 컬렉션 또한 함께 조회한다.

컬렉션 페치 조인은 중복으로 조회될 수 있다.

  • Team 테이블에서의 ‘팀A’는 하나지만 Member 테이블과 조인하면서 결과가 증가하면서, 중복이 된다.

이를 Distinct로 해결할 수 있다.

select distnict t from Team t join fetch t.members where t.name = '팀A'

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

JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 select 절에 지정한 엔티티만 조회한다. 그렇기에 쿼리를 한번 더 날리게 된다.

반면에 페치 조인은 연관된 엔티티도 함께 조회한다.

  • 페치 조인 특징
    • SQL 호출 횟수를 줄여 성능을 최적화 할 수 있다.
    • 페치 조인은 글로벌 로딩 전략보다 우선한다.
      • 로딩 전략(즉시 로딩, 지연 로딩)은 애플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라 부른다.
    • 글로벌 로딩 전략은 지연 로딩으로 하고, 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다.
  • 페치 조인 단점
    • 페치 조인 대상에서는 별칭을 줄 수 없다.
    • 둘 이상의 컬렉션을 페치할 수 없다.
    • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 억지로 페치 조인을 사용하기보다는 여러 테이블에서 필요한 필드들만 조회해서 DTO로 반환하는 것이 더 효과적일 수 있다.

✅경로 표현식

  • 쉽게 이야기 해서 .(점)을 찍어서 객체 그래프를 탐색하는 것이다.

  • 상태 필드 → 단순히 값을 저장하기 위한 필드

    • ex) m.username
    • 경로 탐색의 끝이다. 더는 탐색할 수 없다.
  • 연관 필드 → 연관관계를 위한 필드, 임베디드 타입 포함

    • 단일 값 연관 필드
      • ex) m.team
      • 묵시적으로 내부 조인이 일어난다. 계속 탐색할 수 있다.
      • 계속 탐색 할 수 있다는 것은 m.team.id 이런식으로 탐색한다는 것이다.
    • 컬렉션 값 연관 필드
      • ex) t.members
      • 묵시적으로 내부 조인이 일어나고, 더는 탐색할 수 없다.
  • 컬렉션 값에서 경로 탐색은 불가능 하다.

    select t.members.username from Team t // 실패
    
    select t.members from Team t // 성공
  • 컬렉션까지는 경로 탐색이 가능하며, 만약 컬렉션에서 경로 탐색을 하고 싶다면 조인을 사용해서 새로운 별칭을 획득 해야 한다.

    select m.username from Team t join t.members m

✅Spring data jpa에서의 JPQL

레퍼지토리 메서드에 직접 쿼리를 정의하려면, @Query 어노테이션을 사용한다.

@Query 어노테이션은 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 장점이 있다.

스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원한다.

이름 기반 파라미터 바인딩을 사용하려면, @Param(파라미터 이름) 어노테이션을 사용하면 된다.

@Query("select m from Member m where m.username = :name")
Member findByUsername(@Param("name") String username);


프로젝트에 직접 적용시켜보기 with 조회 최적화

기존 SQL 문과 변경 후 SQL 문을 비교해보자.

기존 SQL문

LetterRepository.class

Optional<List<Letter>> findByRoom(Room room);

쿼리

select
        letter0_.letter_id as letter_i1_3_,
        letter0_.content as content2_3_,
        letter0_.created_at as created_3_3_,
        letter0_.room_id as room_id5_3_,
        letter0_.updated_at as updated_4_3_,
        letter0_.writer_id as writer_i6_3_ 
    from
        letter letter0_ 
    where
        letter0_.room_id=?
  • 이것도 마찬가지로 인자로 받은 Room이 있는 Letter만 찾으므로 → 최적화 X
  • 만약 room도 쓰려면 또 room에 대한 쿼리도 날려야 함.

RoomRepository.class

Optional<Room> findByWriterAndReceiver(Member writer, Member receiver);

쿼리

select
        room0_.id as id1_5_,
        room0_.created_at as created_2_5_,
        room0_.updated_at as updated_3_5_,
        room0_.last_send_time as last_sen4_5_,
        room0_.receiver_id as receiver5_5_,
        room0_.writer_id as writer_i6_5_ 
    from
        room room0_ 
    where
        room0_.writer_id=? 
        and room0_.receiver_id=?
  • 해당 room은 member들에 존재하지 않으니 fetch join을 통해서 가져올 필요가 없다. → 최적화 X
  • 만약 letters도 쓰려면 또 letters에 대한 쿼리도 날려야 함.

변경 후 SQL문

LetterRepository.class

Optional<List<Letter>> findByRoom1(@Param("room")Room room);

쿼리

/* select
        l 
    from
        Letter l 
    join
        fetch l.room 
    where
        l.room = :room */ select
            letter0_.letter_id as letter_i1_3_0_,
            room1_.id as id1_5_1_,
            letter0_.content as content2_3_0_,
            letter0_.created_at as created_3_3_0_,
            letter0_.room_id as room_id5_3_0_,
            letter0_.updated_at as updated_4_3_0_,
            letter0_.writer_id as writer_i6_3_0_,
            room1_.created_at as created_2_5_1_,
            room1_.updated_at as updated_3_5_1_,
            room1_.last_send_time as last_sen4_5_1_,
            room1_.receiver_id as receiver5_5_1_,
            room1_.writer_id as writer_i6_5_1_ 
        from
            letter letter0_ 
        inner join
            room room1_ 
                on letter0_.room_id=room1_.id 
        where
            letter0_.room_id=?
  • 연관된 room 엔티티도 한번에 가져올 수 있었다. → letter.room.id 등을 사용할 때 별도 쿼리 안날려도 괜춘

RoomRepository.class

@Query("select r from Room r join fetch r.letterList where r.receiver = :receiver and  r.writer = :writer")
Optional<Room> findByWriterAndReceiver1(@Param("writer")Member writer, @Param("receiver")Member receiver);

쿼리

/* select
        r 
    from
        Room r 
    join
        fetch r.letterList 
    where
        r.receiver = :receiver 
        and  r.writer = :writer */ select
            room0_.id as id1_5_0_,
            letterlist1_.letter_id as letter_i1_3_1_,
            room0_.created_at as created_2_5_0_,
            room0_.updated_at as updated_3_5_0_,
            room0_.last_send_time as last_sen4_5_0_,
            room0_.receiver_id as receiver5_5_0_,
            room0_.writer_id as writer_i6_5_0_,
            letterlist1_.content as content2_3_1_,
            letterlist1_.created_at as created_3_3_1_,
            letterlist1_.room_id as room_id5_3_1_,
            letterlist1_.updated_at as updated_4_3_1_,
            letterlist1_.writer_id as writer_i6_3_1_,
            letterlist1_.room_id as room_id5_3_0__,
            letterlist1_.letter_id as letter_i1_3_0__ 
        from
            room room0_ 
        inner join
            letter letterlist1_ 
                on room0_.id=letterlist1_.room_id 
        where
            room0_.receiver_id=? 
            and room0_.writer_id=?
  • 연관된 letter 컬렉션 엔티티도 한번에 가져올 수 있었다. letters 사용할 때 별도 쿼리 안날려도 괜춘
profile
Hello, World! \n

0개의 댓글