[Spring] Spring Data JPA 활용

Miin·2023년 11월 19일
0

Spring

목록 보기
10/17
post-custom-banner

Spring Data JPA

JPQL

JPQL이란 JPA Query Language의 줄임말, JPA에서 사용할 수 있는 쿼리.

문법은 SQL과 매우 비슷하다. SQL과의 차이점은 SQL에서는 테이블이나 컬럼의 이름을 사용하는 것과 달리 JPQL은 다음과 같이 엔티티 객체를 대상으로 수행하는 쿼리이기에 매핑된 엔티티의 이름과 필드의 이름을 사용한다는 것이다.

SELECT p FROM Product(엔티티 타입) p WHERE p.number(엔티티 속성) = ?1;

쿼리 메서드

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

  • 주제(Subject)
  • 서술어(Predicate)

'find...By', 'exist...By'와 같은 키워드로 쿼리의 주제를 정함.
'By' - 서술어의 시작을 나타내는 구분자 역할.
서술어 부분은 검색 및 정렬 조건을 지정하는 영역. 엔티티 속성 정의, AND나 OR 사용해 조건 확장 가능

예) (리턴타입) + {주제+서술어(속성)} 구조의 메서드

List<Person> findByLastnameAndEmail(String lastName, String email);

서술어에 들어가는 엔티티 속성 식(Expression)은 위와 같이 엔티티에서 관리하고 있는 속성(필드)만 참조 가능

쿼리 메서드의 주제 키워드

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

find...By

조회하는 기능을 수행하는 키워드. '...'으로 표시한 영역에는 도메인(엔티티) 표현. 그러나 리포지토리에서 이미 도메인을 설정한 후에 메서드를 사용하기 때문에 중복으로 판단해 생략하기도 함. 리턴 타입으로 Collection이나 Stream에 속한 하위 타입 설정 가능.

// 리포지토리에 쿼리 메서드 작성
Optional<Product> findByNumber(Long number);
List<Product> findAllByName(String name);
Product queryByNumber(Long number);

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.>...

쿼리를 통해 조회된 결과값의 개수를 제한하는 키워드. 두 키워드는 동일한 동작 수행. 주제와 By 사이에 위치함. 일반적으로 한 번의 동작으로 여러 건을 조회할 때 사용, 단 건 조회는 <number.> 생략.

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

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

JPQL의 서술어 부분에서 사용

Is

값의 일치를 조건으로 사용하는 조건자 키워드. 생략되는 경우가 많으며 Equals와 동일한 기능 수행.

// findByNumber 메서드와 동일하게 동작
Product findByNumberIs(Long number); 
Product findByNumberEquals(Long number);

(Is)Not

값의 불일치를 조건으로 사용하는 조건자 키워드. Is는 생략하고 Not 키워드만 사용 가능.

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

(Is)Null, (Is)NotNull

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

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

(Is)True, (Is)False

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

Product findByisActiveTrue();
Product findByisActiveIsTrue();
Product findByisActiveFalse();
Product findByisActiveIsFalse();

And, Or

여러 조건을 묶을 때 사용.

Product findByNumberAndName(Long number, String name);
Product findByNumberOrName(Long number, String name);

(Is)Greater Than, (Is)LessThan, (Is)Between

숫자나 datetime 컬럼을 대상으로 한 비교 연산에 사용할 수 있는 조건자 키워드.
GreaterThan, LessThan 키워드는 비교 대상에 대한 초과/미만의 개념으로 비교 연산 수행, 경계값을 포함하려면 Equal 키워드 추가.

List<Product> findByPriceIsGreaterThan(Long price);
List<Product> findByPriceGreaterThan(Long price);
List<Product> findByPriceGreaterThanEqual(Long price);
List<Product> findByPriceIsLessThan(Long price);
List<Product> findByPriceLessThan(Long price);
List<Product> findByPriceLessThanEqual(Long price);
List<Product> findByPriceIsBetween(Long lowPrice, Long highPrice);
List<Product> findByPriceBetween(Long lowPrice, Long highPrice);

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

컬럼값에서 일부 일치 여부를 확인하는 조건자 키워드.
SQL 쿼리문에서 값의 일부를 포함하는 값을 추출할 때 사용하는 '%' 키워드와 동일한 역할을 하는 키워드.
자동으로 생성되는 SQL문을 보면 Containing 키워드는 문자열의 양 끝, StartingWith 키워드는 문자열의 앞, EndingWith 키워드는 문자열의 끝에 '%'가 배치됨. 여기서 별도로 고려해야 하는 키워드는 Like 키워드인데, 이 키워드는 코드 수준에서 메소드를 호출하면서 전달하는 값에 %를 명시적으로 입력해야 함.

List<Product> findByNameLike(String name);
List<Product> findByNameIsLike(String name);

List<Product> findByNameContains(String name);
List<Product> findByNameContaining(String name);
List<Product> findByNameIsContaining(String name);

List<Product> findByNameStartsWith(String name);
List<Product> findByNameStartingWith(String name);
List<Product> findByNameIsStartingWith(String name);

List<Product> findByNameEndsWith(String name);
List<Product> findByNameEndingWith(String name);
List<Product> findByNameIsEndingWith(String name);

정렬 처리

ORDER BY

기본 쿼리 메서드 작성 후 OrderBy 키워드 삽입, 정렬하고자 하는 칼럼과 오름차순/내림차순 설정

List<Product> findByNameOrderByNumberAsc(String name); // Asc : 오름차순 // 상품정보를 이름으로 검색한 후 상품 번호로 오름차순 정렬
List<Product> findByNameOrderByNumberDesc(String name); // Desc : 내림차순

조건 여러 개 사용시 And나 Or 키워드 사용X

List<Product> findByNameOrderByPriceAscStockDesc(String name); // 먼저 Price를 기준으로 오름차순 정렬 후 후순위로 재고수량을 기준으로 내림차순 정렬 수행

매개변수를 활용한 쿼리 정렬

List<Product> findByName(String name, Sort sort);

이름에 키워드를 넣지 않고 Sort 객체를 활용해 매개변수로 받아들인 정렬 기준을 가지고 쿼리문 작성. 쿼리 메서드를 정의하는 단계에서 코드가 줄어드는 장점이 있음.


테스트 코드

@SpringBootTest
public class ProductRepositoryTest0 {

    @Autowired
    ProductRepository productRepository;

    @Test
    void sortingAndPagingTest() {
        Product product1 = new Product();
        product1.setName("펜");
        product1.setPrice(1000);
        product1.setStock(100);
        product1.setCreatedAt(LocalDateTime.now());
        product1.setUpdatedAt(LocalDateTime.now());

        Product product2 = new Product();
        product2.setName("펜");
        product2.setPrice(5000);
        product2.setStock(300);
        product2.setCreatedAt(LocalDateTime.now());
        product2.setUpdatedAt(LocalDateTime.now());

        Product product3 = new Product();
        product3.setName("펜");
        product3.setPrice(500);
        product3.setStock(50);
        product3.setCreatedAt(LocalDateTime.now());
        product3.setUpdatedAt(LocalDateTime.now());

        Product savedProduct1 = productRepository.save(product1);
        Product savedProduct2 = productRepository.save(product2);
        Product savedProduct3 = productRepository.save(product3);
        
        productRepository.findByName("펜", Sort.by(Sort.Order.asc("price")));
        productRepository.findByName("펜", Sort.by(Sort.Order.asc("price"), Sort.Order.desc("stock")));

    }

}

Sort 부분을 하나의 메서드로 분리해 쿼리 메서드를 호출하는 방법

	...상단 코드 동일, 생략
    
		productRepository.findByName("펜", getSort());

	}

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

페이징 처리

페이징이란?

데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것.

ex) 25개 레코드 -> 레코드를 7개씩, 총 4개의 페이지로 구분 / 특정 페이지를 가져옴.

JPA에서는 페이징 처리를 위해 PagePageable 사용.

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

리턴 타입으로 Page를 설정하고 매개변수에는 Pageable 타입의 객체 정의.

// 페이징 쿼리 메서드 호출
Page<Product> productPage = productRepository.findByName("펜", PageRequest.of(0, 2));

리턴 타입으로 Page 객체를 받아야 하기에 Page<Product>로 타입을 선언해 객체를 리턴 받음, Pageable 파라미터를 전달하기 위해 PageRequest 클래스 사용. PageRequestPageable의 구현체.

일반적으로 PageRequestof 메서드를 통해 PageRequest 객체 생성. of 메서드는 매개변수에 따라 다양한 형태로 오버로딩.

  • of(int page, int size): 매개변수[페이지 번호(0부터 시작), 페이지당 데이터 개수] / 데이터 정렬X
  • of(int page, int size, Sort): 매개변수[페이지 번호, 페이지당 데이터 개수, 정렬] / sort에 의해 정렬
  • of(int page, int size, Direction, String... properties): 매개변수[페이지 번호, 페이지당 데이터 개수, 정렬 방향, 속성(컬럼)] / Sort.by(direction, properties)에 의해 정렬

Page 객체의 데이터 출력

Page 객체를 그대로 출력하면 해당 객체의 값을 보여주지 않고 몇 번째 페이지에 해당하는지만 확인 가능.

Page<Product> productPage = productRepository.findByName("펜", PageRequest.of(0, 2));
        System.out.println(productPage.getContent()); // 배열 형태로 값 출력



@Query 어노테이션 사용하기

@Query 어노테이션을 사용해 직접 JPQL을 작성할 수 있음.

@Query 어노테이션을 사용하는 메서드

	@Query("SELECT p FROM Product AS p WHERE p.name = ?1")
    List<Product> findByName(String name);

FROM 뒤에서 엔티티 타입 지정, 별칭 생성(AS 생략 가능).
WHERE문에서는 SQL과 마찬가지로 조건 지정.

조건문에서 사용한 '?1'은 파라미터를 전달받기 위한 인자에 해당, 1은 첫 번째 파라미터 의미. 파라미터의 순서가 바뀌면 오류 발생 가능성이 있어 다음과 같이 @Param 어노테이션을 사용하는 것이 좋음.

	@Query("SELECT p FROM Product AS p WHERE p.name = :name")
    List<Product> findByNameParam(@Param("name") String name);

파라미터를 바인딩하는 방식으로 메서드를 구현하면 코드의 가독성이 높아지고 유지보수가 수월해짐.

특정 컬럼만 추출하는 쿼리

@Query를 사용하면 엔티티 타입이 아니라 원하는 컬럼의 값만 추출 가능.

	@Query("SELECT p.name, p.price, p.stock FROM Product p WHERE p.name = :name")
    List<Object[]> findByNameParam2(@Param("name") String name);

SELECT에 가져오고자 하는 컬럼 지정. 이때 메서드에서는 Object 배열의 리스트 형태로 리턴 타입을 지정해야 함.



출처 - (책) 스프링 부트 핵심 가이드 / 장정우, 위키북스

profile
컴퓨터공학전공 학부생 Back-end Developer
post-custom-banner

0개의 댓글