Spring Data JPA 활용

김하영·2023년 6월 11일

출처: 스프링 부트 핵심 가이드 - 장정우 지음
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=296591989

JPQL

JPQL은 JPA Query Language의 줄임말로 JPA에서 사용할 수 있는 쿼리를 의미한다.
JPQL의 문법은 sql과 비슷해서 DB쿼리에 익숙하다면 어렵지 않게 사용할 수 있다.

<sql JPQL의 차이>

sql과의 차이점은 sql에서는 테이블이나 칼럼의 이름을 사용하지만,
JPQL은 엔티티 객체를 대상으로 수행하는 쿼리이기 때문에 매핑된 엔티티의 이름과 필드의 이름을 사용한다.

JPQL 예시) 여기서 Product는 엔티티 타입이다.
SELECT p FROM Product p WHERE p.number = ?1;

쿼리 메서드

리포지토리는 JpaRepository를 상속받는 것만으로도 다양한 CRUD메소드를 제공한다.
하지만 기본 메소드는 식별자 기반으로 생성되므로 별도의 메소드를 정의해서 사용하는 경우가 있다.
이때, 간단한 쿼리문을 작성하기 위해 사용되는 것이 쿼리메소드이다.

조회 기능 주요 키워드

'...'으로 표시한 영역에는 도메인(엔티티)를 표현할 수 있다.
그러나 리포지토리에 이미 도메인을 설정한 후에 메소드를 사용하므로 생략하는 경우가 많다.
리턴타입으로는 Collection이나 Stream에 속한 하위타입을 설정할 수 있다.

  • find ... By
  • get ... By
  • read ... By
  • query ... By
  • search ... By
  • stream ... By

exists...By

특정 데이터의 존재여부 확인하는 키워드로, 리턴타입으로는 boolean타입을 사용한다.

boolean existsByNumber(Long number);

count...By

조회 쿼리 수행 후 쿼리 결과로 나온 레코드 수를 리턴한다.

long countByName(String name);

delete...By, remove...By

삭제 쿼리를 수행한다. 리턴타입이 없거나 삭제한 횟수를 리턴한다.

void deleteByNumber(Long number);
long removeByName(String name);

...First<number>..., ...Top<number>...

쿼리를 통해 조회된 결과값의 개수를 제한하는 키워드이다.
두 키워드는 동일한 동작을 수행한다.
이 키워드는 한번의 동작으로 여러건을 조회할때 사용되며, 한건을 조회하기 위해서는 <number>를 생략하면 된다.

List<Product> findFirst5ByName(String name);
List<Product>findTop10ByName(String name);

쿼리메소드의 조건자 키워드

Is

값의 일치를 조건으로 사용하는 키워드이다.
생략되는 경우가 많고, Equals와 동일한 기능을 한다.

Product findByNumberIs(Long number);
Product findByNumberEquals(Long number);

(Is)Not

값의 불일치를 조건으로 사용하는 키워드이다.

Product findByNumberIsNot(Long number);
Product findByNumberNot(Long number);

(Is)Null, (Is)NotNull

값이 Null인지 검사하는 조건자 키워드이다.

List<Product> findByUpdatedAtNull();
List<Product> findByUpdatedAtNotNull();

(Is)True, (Is)False

boolean 타입으로 지정된 칼럼값을 확인하는 키워드

Product findByisActiveTrue();
Product findByisActiveFalse();

And, Or

여러 조건을 묶을 때 사용

Product findByNameAndNumber(String name, Long number);
Product findByNameOrNumber(String name, Long number);

(Is)GreaterThan, (Is)LessThan, (Is)Between

숫자나 datetime칼럼을 대상으로 한 비교연산에 사용할 수 있는 조건자 키워드.
경계값을 포함하려면 Equal키워드를 추가하면 된다.

List<Product> findByPriceGreaterThan(Long price); // 초과
List<Product> findByPriceLessThan(Long price); // 미만

List<Product> findByPriceGreaterThanEqual(Long price); // 이상
List<Product> findByPriceLessThanEqual(Long price); // 이하

List<Product> findByPriceBetween(Long lowPrice, Long highPrice);

(Is)StartingWith(==StartsWith), (Is)EndingWith(==EndsWith), (Is)Containing(==Contains), (Is)Like

칼럼값에서 일부 일치 여부를 확인하는 조건자 키워드이다.
sql쿼리문에서 값의 일부를 포함하는 %와 같은 역할을 한다.
Like 키워드에서는, 코드수준에서 메소드를 호출하면서 전달하는 값에 %를 명시적으로 입력해야한다.

// Like
List<Product> findByNameLike(String name);
// Contains
List<Product> findByNameContaining(String name);
List<Product> findByNameContains(String name);
// starting with
List<Product> findByNameStartingWith(String name);
List<Product> findByNameStartsWith(String name);
// ending with
List<Product> findByNameEndingWith(String name);
List<Product> findByNameEndsWith(String name);

정렬과 페이징

정렬 처리하기

OrderBy로 정렬

일반적인 쿼리문에서 정렬을 사용할 때는 order by 구문을 사용한다.
쿼리 메소드에서도 동일하게 사용하면 된다.

// Asc: 오름차순, Desc: 내림차순
List<Product> findByNameOrderByNumberAsc(String name);
List<Product> findByNameOrderByNumberDesc(String name);

다른 쿼리들은 조건을 여러개 사용하기 위해 And, Or을 사용했지만,
정렬구문은 우선순위를 기준으로 차례대로 작성하면 된다.

// 숫자 오름차순, 숫자가 같다면 가격 내림차순 순으로 정렬 수행
List<Product> findByNameOrderByPriceAscStockDesc(String name);

Sort객체 사용하여 정렬

메소드의 이름이 길어질수록 OrderBy로 정렬하면 가독성이 떨어진다.
Sort객체를 활용해서 매개변수로 받아들인 정렬 기준을 가지고 쿼리문을 작성할 수 있다.
Sort 클래스는 내부 클래스로 정의 되어있는 Order객체를 활용하여 정렬기준을 생성한다.

// 리포지토리
List<Product> findByName(String name, Sort sort);
// 태스트 수행
productRepository.findByName("공책", Sort.by(Order.asc("price"), Order.desc("stock")));

Sort부분을 하나의 메소드로 분리 할 수 도 있다.

// 리포지토리
List<Product> findByName(String name, Sort sort);

// 태스트 수행
productRepository.findByName("공책", getSort());

private Sort getSort(){
	return Sort.by(Order.asc("price"), Order.desc("stock"));
}

페이징 처리하기

페이징이란 데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것을 의미한다.

JPA에서는 페이징 처리를 위해 Page와 Pageable을 사용한다.
리턴 타입으로 Page를 설정하고, 매개변수에는 Pageable타입의 객체를 정의한다.

// 페이징 처리를 위한 쿼리메소드 예시
Page<Product> findByName(String name, Pageable pageable);

// 페이징 쿼리메소드 호출
// PageRequest: Pageable 구현체, 페이지 번호와 페이지당 데이터 개수를 매개변수로 받음
Page<Product> productPage = productRepository.findByName("공책", PageRequest.of(0,2));

페이지 객체를 그대로 출력하면 해당객체의 값을 보여주지않고 몇번째 펭지에 해당하는지만 알 수 있다.
세부적인 값을 보려면 다음과 같이 작성한다.
getContent() 메소드를 사용하면 배열형태로 값이 출력된다.

// 세부 데이터 출력
System.out.println(productPage.getContent());

@Query 어노테이션 사용하기

데이터베이스에서 값을 가져올 때 앞에 설명 했듯이 메소드 이름만으로 쿼리 메소드를 생성할 수도 있다.
이번에는 @Query어노테이션을 사용하여 직접 JPQL을 작성해보자.

JPQL을 사용하면 JPA 구현체에서 자동으로 쿼리문장을 해석하고 실행한다.
하지만 직접 sql문을 작성해야할 경우도 있다.

<직접 sql 쿼리를 작성하는 경우 - @Query 어노테이션을 사용하는 경우>

  • 만약 데이터베이스를 다른걸로 변경할 일이 없다면, 직접 해당 DB에 특화된 sql을 작성할 수 있다!
  • 또 주로 튜닝된 쿼리를 사용하고자 할때 직접 sql을 작성한다.
  • @Query를 사용하면 엔티티 타입이 아니라 원하는 칼럼의 값만 추출할 수 있다

// @Query 어노테이션 사용하는 메소드
// ?1: 파라미터를 전달받기위한 인자
@Query("SELECT p FROM Product AS p WHERE p.name = ?1")
List<Product> findByName(String name);

위와 같이 "?1"과 같은 방식으로 인자를 받는 방식을 사용하면, 파라미터의 순서가 바뀌면 오류가 발생할 기능성이 있으므로 @Param 어노테이션을 사용하는 것이 좋다.

// @Query 어노테이션 사용하는 메소드
// @Param어노테이션으로 인자 받기
@Query("SELECT p FROM Product AS p WHERE p.name = :name")
List<Product> findByName(@Param("name") String name);

이렇게 파라미터를 바인딩하는 방식으로 메소드를 구현하면 코드의 가독성이 높아지고 유지보수가 수월해진다!

그리고 @Query를 사용하면 엔티티 타입이 아니라 원하는 칼럼의 값만 추출할 수 있다.
아래처럼 Select에 가져오고자 하는 칼럼을 지정하면된다.
이때 메서드에서는 Object배열의 리스트 형태로 리턴 타입을 지정해야 한다!

// @Query 어노테이션 사용하는 메소드
// @Param어노테이션으로 인자 받기
// 원하는 칼럼만 받아오기 위해 리턴타입을 List<Object[]>로 해주기
@Query("SELECT p.name, p.price, p.stock FROM Product p WHERE p.name = :name")
List<Object[]> findByName(@Param("name") String name);
profile
백엔드 개발자로 일하고 싶어요 제발

0개의 댓글