어떤 방법을 사용하든 JPQL에서 모든 것이 시작된다.
JPQL도 비슷하게, SELECT, UPDATE, DELETE 문을 사용할 수 있다.
엔티티를 저장할 때는 EntityManager.persist() 메서드를 사용하면 되므로, INSERT 문은 없다.
엔티티와 속성은 대소문자를 구분하고, SELECT, FROM, AS와 같은 JPQL 키워드는 대소문자를 구분 하지 않는다.
JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다. 엔티티 명을 지정하지 않으면 클래스 명을 기본값으로 사용한다.
별칭은 필수이다. 별칭은 From 엔티티 AS 뒤에 지정할 수 있으며, AS는 생략 가능하다.
SELECT 문
SELECT m FROM Member As m where m.username = 'HELLO'
작성한 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 // 불가능
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 명령어를 사용하면, 객체 변환 작업을 쉽게 처리할 수 있다.
TypeQuery<UserDTO> query = em.createQuery("select new jpabook.jpql.userDTO(m.username, m.age)
from Member m", UserDTO.class);
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();
}
//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
→ 결과 수를 구한다. 반환 타입 : LongMAX
, MIN
→ 최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용AVG
→ 평균값을 구한다. 숫자 타입만 사용가능하고, 반환 타입 : DoubleSUM
→ 합을 구한다.Group By → 통계 데이터를 구할 때 특정 그룹끼리 묶어준다.
Having → Group By로 그룹화 한 통계 데이터를 기준으로 필터링 한다.
통계 쿼리는 보통 전체 데이터를 기준으로 처리하므로, 실시간으로 사용하기엔 부담이 많다. 결과가 아주 많다면 통계 결과만 저장하는 테이블을 별도로 만들어 두고, 사용자가 적은 새벽에 통계 쿼리를 실행해서 그 결과를 보관하는 것이 좋다.
정렬
Order By는 결과를 정렬할 때 사용한다.
select m from Member m order by m.age DESC, m.username ASC
내부 조인
select m from Member m inner join m.team t where t.name = :teamName;
JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다.
그러면 내부 조인을 사용하는게 성능 상 더 좋은 것 아닌가?
내부 조인 (Inner Join):
내부 조인은 두 테이블 사이에서 일치하는 행만 반환하는 조인 유형입니다. 즉, 조인 조건을 만족하는 데이터만 결과로 나타납니다. 내부 조인은 주로 데이터베이스 엔진에 의해 최적화되어 빠른 성능을 보이기 때문에, 대부분의 경우에서 내부 조인이 더 빠른 실행 속도를 가질 수 있습니다.
외부 조인 (Outer Join):
외부 조인은 두 테이블 사이에서 일치하지 않는 행도 포함하여 결과를 반환합니다. 왼쪽 테이블의 모든 행을 포함하는 왼쪽 외부 조인과 오른쪽 테이블의 모든 행을 포함하는 오른쪽 외부 조인, 그리고 양쪽 테이블의 모든 행을 포함하는 전체 외부 조인 등이 있습니다. 외부 조인은 모든 데이터를 보존하면서 조인 결과를 생성할 수 있지만, 내부 조인에 비해 조금 더 복잡한 처리가 필요할 수 있어 실행 속도가 느릴 수 있습니다.
그렇다면 왜 외부 조인을 사용할까요? 외부 조인은 다음과 같은 상황에서 유용합니다:
누락된 데이터 확인: 외부 조인을 사용하면 어느 한쪽 테이블에만 있는 데이터도 확인할 수 있습니다. 이를 통해 데이터의 누락이나 오류를 쉽게 파악할 수 있습니다.
결론적으로, 내부 조인은 대부분의 경우에 빠른 성능을 제공하지만, 외부 조인은 데이터의 누락을 확인하거나 보완적인 정보를 얻을 때 유용합니다. 데이터베이스 설계와 쿼리 작성 시에 상황에 맞게 적절한 조인 유형을 선택하는 것이 중요합니다.
외부 조인
select m from Member m left outer join m.team t
컬렉션 조인
N → 1으로의 조인은 다대일 조인이면서, 단일 값 연관 필드를 사용한다. m.team
1 → N으로의 조인은 일대다 조인이면서, 컬렉션 값 연관 필드를 사용한다. m.members
select t, m from Team t left join t.members m
세타 조인
Join On 절
select m, t from Member m left join m.team t on t.name = 'A'
엔티티 페치 조인
select m from Member m join fetch m.team
이 경우에 Member와 Member와 연관된 Team 엔티티를 함께 조회한다.
페치 조인은 별칭을 사용할 수 없다.
연관된 엔티티 또한 함께 내부 조인을 하기에, 쿼리는 한번만 나간다!!!
**컬렉션 페치 조인**
select t from Team t join fetch t.members where t.name = '팀A'
컬렉션 페치 조인은 중복으로 조회될 수 있다.
이를 Distinct로 해결할 수 있다.
select distnict t from Team t join fetch t.members where t.name = '팀A'
JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 select 절에 지정한 엔티티만 조회한다. 그렇기에 쿼리를 한번 더 날리게 된다.
반면에 페치 조인은 연관된 엔티티도 함께 조회한다.
쉽게 이야기 해서 .(점)을 찍어서 객체 그래프를 탐색하는 것이다.
상태 필드 → 단순히 값을 저장하기 위한 필드
연관 필드 → 연관관계를 위한 필드, 임베디드 타입 포함
m.team
m.team.id
이런식으로 탐색한다는 것이다.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
레퍼지토리 메서드에 직접 쿼리를 정의하려면, @Query
어노테이션을 사용한다.
@Query
어노테이션은 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 장점이 있다.
스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원한다.
이름 기반 파라미터 바인딩을 사용하려면, @Param(파라미터 이름)
어노테이션을 사용하면 된다.
@Query("select m from Member m where m.username = :name")
Member findByUsername(@Param("name") String username);
기존 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=?
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=?
변경 후 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=?
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=?