SKSDUD
0912 화요일 - 고난과 역경(2) / JPQL & @Query 본문
(물)경력직으로 들어갔는데 모르면
ㄷㅓ 괴롭겠지
신입일 때 공부하자
📝
1️⃣
개념공부
이전 포스팅에서 하다가 말았는데 오늘 완전히 정리해 보겠다. 이렇게 노는 것도 목요일이 끝이다.
Query란 직역 하면 "질의문"이라는 뜻으로. 풀어쓰자면 저장된 데이터를 필터링하기 위한 질문을 말한다.
아래와 같이 레포지토리에서 JpaRepository를 상속(extends)받음으로써 기본적인 CRUD를 사용할 수 있다.
.save() .findAll() .findById() 등
@Repository
public interface BoardRepository extends JpaRepository<Board, Long>, JpaSpecificationExecutor<Board> {}
그러나, JpaRepository가 지원하지 않는 기능들을 사용해야 할 때는 사용자 정의 쿼리를 사용해야 한다!
사용자 정의 쿼리란 JPA가 자동으로 생성하는 쿼리(쿼리 메서드)를 사용하는 게 아닌 사용자가 정의한 대로 쿼리가 생성 혹은 데이터베이스에 종속적인 Native Query가 생성되는 것을 말한다. 이를 구현하기 위해서는
1. JPQL을 작성해 쿼리 실행@Query를 사용해 사용할 메서드 위에 정의
2. @Query를 사용해 사용할 메서드 위에 정의
두가지 방법이 있다.
JPQL
Java 기반의 객체 관계 매핑(ORM) 기술인 JPA를 사용하여 데이터베이스와 상호 작용하기 위한 쿼리 언어이다.
JPQL과 SQL은 차이점이 있는데
SQL
테이블을 대상으로 쿼리 실행
JPQL
테이블이 아닌 엔티티(객체)를 대상으로 쿼리 실행
JPQL은 SQL과 문법이 유사하지만 몇 가지 다른 특징이 있다.
- 대소문자 구분
- 엔티티와 속성에 대해서 대소문자를 구분한다.
- SELECT, WHERE과 같은 JPQL 키워드는 대소문자를 구부하지 않아도 된다.
- 엔티티 명시
- JPQL에서 사용한 Member 클래스는 이름이 아닌 엔티티 이름이다. 엔티티 이름은 @Entity(name = "이름") name 속성으로 설정 가능하다.
- 별칭 필수
- 별칭을 필수적으로 지정해주어야 한다.
- 별칭을 명시하는 AS 키워드는 생략 가능하다.
@Query
@Query 어노테이션은 JpaRepository를 상속하는 인터페이스에서 사용한다. 위의 JPQL 객체지향 쿼리 언어를 통해 복잡한 쿼리 처리를 지원한다. 실행할 메서드 위에 정적 쿼리를 작성한다. 사용자 지정 쿼리를 만들기 위해 파라미터 바인딩을 하는데
파라미터 바인딩이란?!
@Repository
public interface CarbonRecordRepository extends JpaRepository<CarbonRecord, Long> {
@Query("SELECT DISTINCT cr.category2 FROM CarbonRecord cr WHERE cr.category1 = :category1")
List<String> findDistinctCategory2ByCategory1(@Param("category1") String category1);
}
위의 findDistinctCategory2ByCategory1() 메서드 안에 들어갈 파라미터를 우리가 작성할 쿼리문에 넣는 것을 말한다.
👀 잠깐!
바보라서 이런것도 정리하고 가야 한다.
매개변수와 인수는 다르다. 파라미터와 아규먼트는 다르다.
매개변수는 함수를 정의할 때 사용되는 변수를 말한다.
인수는 실제로 함수가 호출될 때 넘기는 변숫값을 인수라고 한다.
Oracle 공식 홈페이지는 다음과 같이 정의한다.
"매개변수는 메서드 선언의 변수 목록을 나타냅니다. 인수는 메서드가 호출될 때 전달되는 실제 값입니다."
호오 그렇군
아무튼 사용자 정의 쿼리가 되기 위해서는 함수 안에 쓰이는 파라미터를 쿼리문 안에 넣어야 하는데 두 가지 방법이 있다.
- 위치 기준 바인딩
- 이름 기준 바인딩
두 가지 방법이 있다. 전자의 위치를 잘못 기재하여 발생하는 오류와 수정이 어렵고 가독성 측면에서도 이름 바인딩으로 주로 쓴다고 한다. 또한 nativeQuery = true 옵션을 통해 DB에서 쿼리문을 작성하는 방식으로 작성할 수 있다.
📝 추가
쿼리 메서드(Query Method)
메서드 이름으로 우리가 원하는 기능을 수행할 쿼리가 자동으로 생성되게 할 수 있다.
Spring Data JPA에서 정해놓은 네이밍 컨벤션을 지키면 JPA가 해당 메서드 이름을 분석해서 적절한 JPQL을 구성한다.
하지만 쿼리 메서드 기능이 강력하다고 하여도 모든 사용자의 니즈를 파악하기 힘들고 Native Query를 사용해야 하는 순간도 많다.
정리) 쿼리문은 기본으로 set 되어 있거나 메서드 이름만 명명하면 자동으로 만들어주거나 사용자 정의 쿼리로 만들거나
♾️ 참고 사이트
JPA와 사용자 정의 쿼리
JPA와 사용자 정의 쿼리
velog.io
JPA에서 쿼리를 쓰는 법
JPA에서 쿼리를 쓰는 법 KeywordSampleJPQL snippet `And` `findByLastnameAndFirstname` `… where x.lastname = ?1 and x.firstname = ?2` `Or` `findByLastnameOrFirstname` `… where x.lastname = ?1 or x.firstname = ?2` `Is`, `Equals` `findByFirstname`,`
wonin.tistory.com
2️⃣
개념공부는 끝났다.
실제 코드에서 쿼리문 파악하기
- JPQL
- @Query
- 쿼리 메서드
JPQL
먼저 JPQL 코드를 살펴보자
JPQL은 SQL 포맷은 유지하되, 테이블이 아닌 엔티티를 대상으로 한다.
@Repository
public List<IpccRecordSummaryVM> getIpccRecordsSummaryOnRegion(IpccRecordReportRequest ipccRecordReportRequest) {
JPQLQuery query = getQueryFactory()
.select(Projections.fields(IpccRecordSummaryVM.class,
ipccRecord.region.as("name"),
ipccRecord.value.sum().as("value")
))
.from(ipccRecord)
.where(
excludeRegion(ipccRecordReportRequest.getNotRegion()),
yearEq(ipccRecordReportRequest.getYear()),
category1Eq(ipccRecordReportRequest.getCategory1()),
excludeCategory1Eq(ipccRecordReportRequest.getNotCategory1()),
betweenYear(ipccRecordReportRequest.getFromYear(), ipccRecordReportRequest.getToYear())
);
query.groupBy(ipccRecord.region);
return query.fetch();
}
위 코드는 JPQL(Java Persistence Query Language) 쿼리를 사용하여 데이터베이스에서 'ipccRecord' 엔티티티를 검색하고 요약된 결과를 반환하는 메서드이다.
.form(ipccRecord)
from 메서드는 데이터를 검색할 엔티티를 지정한다.
1. 'getIpccRecordsSummaryRegion' 메서드
'IpccRecordReportRequest' 객체를 매개변수로 받아서 해당 요청에 따른 결과를 검색하고 반환합니다.
결과는 'IpccRecordSummaryVM' 클래스의 리스트 목록으로 반환됩니다.
@Repository
public List<IpccRecordSummaryVM> getIpccRecordsSummaryOnRegion(IpccRecordReportRequest ipccRecordReportRequest) {
// TODO : 쿼리문을 구현하시오
}
2. 'JPQLQuery query = getQueryFactory()'
JPQL 쿼리를 생성하기 위한 Querydsl 라이브러리의 'JPQLQuery' 객체를 생성합니다. Querydsl은 JPQL 쿼리를 Java 코드로 작성할 수 있도록 도와주는 라이브러리이다.
3. 'select(Projections.fields(IpccRecordSummaryVM.class, ...))'
select 메서드는 검색할 엔티티 및 결과로 반환할 필드를 정의한다. 여기서 'IpccRecordSummaryVM 클래스의 필드를 선택하였다. Projections.fields를 사용하여 결과를 IpccRecordSummaryVM 클래스의 필드로 매핑하고 있다.
ipccRecord.region.as("name") : ipccRecord 객체에서 region 필드를 선택하고 결과를 "name"이라는 별칭으로 지정합니다.
ipccRecord.value.sum().as("value") : ipccRecord 객체에서 value 필드를 선택하고 해당 필드의 합계를 구한 후 결과를 "value"라는 별칭으로 지정합니다.
이렇게 설정된 select 구문은 데이터베이스 쿼리에서 원하는 열을 선택하고 이 열들을 IpccRecordSummaryVM 객체의 필드에 매핑하여 새로운 객체를 생성합니다. 결과적으로, 이 메서드는 데이터베이스로부터 가져온 데이터를 IpccRecordSummaryVM 객체의 목록으로 반환합니다.
4. 'where'
'where' 메서드를 사용하여 검색 조건을 지정한다. 검색조건을 구체화할 수 있는 여러 가지 사용자 정의 메서드가 있다.
5. 'query.groupBy(ipccRecord.region)'
groupBy 메서드를 사용하여 결과를 그룹화합니다. 여기서는 ipccRecord.region 기준으로 그룹화하고 있으며 name 필드로 그룹화한 결과를 가져온다.
6. 'return query.fetch()'
최종적으로 'query.fetch()' 메서드를 호출하여 쿼리를 실행하고 결과를 가져온다. 결과는 'IpccRecordSummaryVM' 클래스의 인스턴스로 매핑되고, 이러한 결과를 리스트로 반환한다.
@Query
그다음은 @Query 애노테이션으로 만든 쿼리문이다.
@Query는 Spring Data JPA에서 사용되며, 사용자가 직접 JPQL(Java Persistence Query Language) 쿼리를 작성하여 데이터베이스에서 데이터를 조회하고 조작하는 데 사용된다.
@Repository
public interface InventoryFileRecordRepository extends JpaRepository<InventoryFileRecord, Long> {
List<InventoryFileRecord> findAllByInventoryFileIdOrderByCodeAsc(Long inventoryFileId);
@Query("SELECT DISTINCT ifr.year FROM InventoryFileRecord ifr WHERE ifr.inventoryFile.id = :id")
List<Integer> findDistinctYearByInventoryFileId(@Param("id") Long inventoryFileId);
@Query("SELECT DISTINCT ifr.sheetName FROM InventoryFileRecord ifr WHERE ifr.inventoryFile.id = :id")
List<String> findDistinctSheetNameByInventoryFileId(@Param("id") Long inventoryFileId);
Page<InventoryFileRecord> findAllBySheetNameAndInventoryFileIdOrderByIdAsc(String sheetName, Long inventoryFileId, Pageable pageable);
}
:id는 쿼리 파라미터로 사용되며 나중에 '@Param("id") Long inventoryFileId'에서 정의된 파라미터 값으로 대체된다.
3️⃣
Repository 모두 파악하기
@Repository
public interface CarbonRecordRepository extends JpaRepository<CarbonRecord, Long> {
@Query("SELECT DISTINCT cr.category2 FROM CarbonRecord cr WHERE cr.category1 = :category1")
List<String> findDistinctCategory2ByCategory1(@Param("category1") String category1);
}
@Query 애너테이션을 이용해 사용자 정의 쿼리문을 작성했다.
CarbonRecord 엔티티의 별칭인 cr을 정의했다.
cr 엔티티의 필드인 category2를 데이터 중복 없이 조회한다.
검색조건으로 category1 필드가 category1 파라미터(매개변수) 값과 일치하는 경우에 해당한다.
@Service
public List<String> getCarbonRecordsGroupByCategory2(String category1) {
return carbonRecordRepository.findDistinctCategory2ByCategory1(category1);
}
서비스 단에서 위와 같이 활용된다!