JPA - JpaRepository

최상민·2023년 11월 28일
0

Spring Boot

목록 보기
4/5

자바 웹 개발 워크북 - 예스24
아래내용은 ‘자바 웹 개발 워크북(구멍가게 코딩단)’ 책의 pp. 432 ~ 508 (5.3. Spring Data JPA ~ 5.4. 게시물 관리 완성하기)의 내용을 토대로 작성되었습니다.

JpaRepository 인터페이스

엔티티의 Repository는 JpaRepository를 상속받아(extends) 기본적인 CRUD와 페이징 처리를 할 수 있다. 기존의 Repository가 @Repository 어노테이션으로 Spring에 빈을 등록했다면, JpaRepository를 상속받는 Repository의 경우 Spring Data JPA에 의해 런타임 시 자동으로 Spring에 빈으로 등록된다. 즉, @Repository 어노테이션 생략이 가능하다.

JpaRepository 인터페이스를 상속하는 엔티티의 인터페이스로 CRUD와 페이징 처리를 할 수 있다.

package com.shop.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import com.shop.domain.Board;
import com.shop.repository.search.BoardSearch;

public interface BoardRepository extends JpaRepository<Board, Long>, BoardSearch {
	// JpaRepository<테이블 타입, 테이블의 PK 타입>을 extends 한다.
}

JpaRepository 기본 메서드

JpaRepository는 다음과 같은 메서드를 기본으로 지원한다.

  • save(Enitity): 엔티티를 삽입 또는 수정. 현재 영속 컨텍스트 내에 데이터가 존재하면 update, 아니면 insert를 자동으로 실행한다. 저장된 엔티티를 반환한다.
  • findById(id): 주어진 id에 해당하는 엔티티 검색(select). Optional<T>(T는 해당 엔티티 타입)를 반환한다.
    • Optional<T>: 값이 존재하지 않을 수 있는 상황에서 null로 인한 NullPointerException을 방지해준다.
    • orElseThrow(): Optional<T> 객체가 비어있는 경우 NoSuchElementException를 발생시키는 메서드이다.
  • findAll(): 해당 테이블의 모든 엔티티 검색(select)
  • findAll(Pageable): 해당 조건에 맞는 엔티티 검색. Page<Entity>를 반환, 반환받은 결과에서 getContent() 메소드를 사용하여 List<Entity>를 추출할 수 있다.
  • deleteById(id): 해당 id를 가진 엔티티를 삭제
  • count(): 테이블의 엔티티 총 개수 확인
💡 save나 delete 메서드를 호출 시 select문이 먼저 실행되며, 해당 엔티티의 존재 여부를 검사한다. delete의 경우 해당 엔티티가 존재하지 않더라도 sql문이 실행되고, `EntityNotFoundException` ****예외가 발생할 수 있다.
  • PageRequest.of(페이지 번호, 페이지 수, 정렬조건): 조건에 맞는 Pageable 객체를 반환하여 findAll에 들어가 페이징 조건을 생성
    • 페이지 번호는 시작할 페이지 번호이다. 만약 페이지 수가 10이라면, 페이지 번호가 0일 때는 인덱스 0~9까지를 가져오고, 1일 때는 인덱스 10~19까지를 가져온다.
    • 페이지 수는 시작 인덱스에서 페이지 수만큼 가져오는 것으로, 시작 인덱스+페이지 수 - 1까지 가져온다. 예컨대 시작 인데스가 0이고, 페이지 수가 10이라면 0~9까지 10개를 가져온다.
    • 정렬조건: Sort.by(컬럼).속성을 사용하여 컬럼을 기준으로 정렬할 수 있다.
  • Pageable: 페이징 조건을 저장하는 객체
  • Page<Entity>: 페이징 조건에 따른 엔티티를 저장하는 객체
    • getTotalElements(): 테이블의 총 엔티티 개수
    • getTotalPages(): 해당 페이징 조건의 페이지 수에에 따른 테이블의 페이지 개수. 예컨대 총 엔티티가 99개이고, 페이징 조건이 10개씩이였다면 총 페이지 개수는 10개이고, 마지막 페이지에는 9개가 들어간다.
    • getNumber(): 현재 페이지 번호. 0부터 시작. PageRequest.of 에서 설정한 페이지 번호
    • getSize(): 페이지 수. PageRequest.of 에서 설정한 페이지 수
    • getContet(): 현재 페이지에 포함된 엔티티를 List<Entity>로 반환. 만약 페이지 수가 10, 총 엔티티가 99라면, 마지막 페이지는 9개의 엔티티만 반환된다.
  • BoardRepositoryTests
    package com.shop.repository;
    
    import lombok.extern.log4j.Log4j2;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.PageRequest;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.domain.Sort;
    import com.shop.domain.Board;
    import com.shop.dto.BoardListReplyCountDTO;
    
    import java.util.List;
    import java.util.Optional;
    import java.util.stream.IntStream;
    
    @SpringBootTest
    @Log4j2
    public class BoardRepositoryTests {
    
        @Autowired
        private BoardRepository boardRepository;
    
        @Test
        public void testInsert() {
            IntStream.rangeClosed(1,100).forEach(i -> {
                Board board = Board.builder()
                        .title("title..." +i)
                        .content("content..." + i)
                        .writer("user"+ (i % 10))
                        .build();
    
                Board result = boardRepository.save(board);
    						// INSERT INTO board (title, content, user) VALUES ('title1', 'content1', 'user0');
                log.info("BNO: " + result.getBno());
            });
        }
    
        @Test
        public void testSelect() {
            Long bno = 100L;
            Optional<Board> result = boardRepository.findById(bno);
            Board board = result.orElseThrow();
    				// SELECT * FROM board WHERE bno = 100;
            log.info(board);
        }
    
        @Test
        public void testUpdate() {
            Long bno = 100L;
            Optional<Board> result = boardRepository.findById(bno);
            Board board = result.orElseThrow();
    				// SELECT * FROM board WHERE bno = 100;
    				board.setTitle("update title 100");
    				board.setContent("update content 100");
    				// bno 100 번의 title, content만 변경
            boardRepository.save(board);
    				// UPDATE board SET title='update title 100', content='update content 100' WHERE bno = 100;
        }
    
        @Test
        public void testDelete() {
            Long bno = 1L;
            boardRepository.deleteById(bno);
    				// DELETE FROM board WHERE bno = 1;
        }
    
        @Test
        public void testPaging() {
            Pageable pageable = PageRequest.of(0,10, Sort.by("bno").descending());
            Page<Board> result = boardRepository.findAll(pageable);
    				// SELECT * FROM board ORDER BY bno DESC LIMIT 0, 10;
    
            log.info("total count: "+result.getTotalElements());
            log.info( "total pages:" +result.getTotalPages());
            log.info("page number: "+result.getNumber());
            log.info("page size: "+result.getSize());
    
            List<Board> boardList = result.getContent();
    
            boardList.forEach(board -> log.info(board));
        }
    }

쿼리메소드

메소드 이름을 통해 쿼리를 자동생성해주는 기능으로 편리하지만, 상당히 길고 복잡한 메소드를 작성하게 되어 실제 개발에서는 많이 사요되지 않는다.

메소드 이름은 기본적인 JPA 메소드와 키워드로 구성된다. 키워드에는 Distinct, Like 등이 있다. 자세한 사항은 공식 문서에서 확인할 수 있다(🔗).

package com.springdata.repository;

import com.springdata.entity.Board;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

public interface BoardRepository extends JpaRepository<Board, Long>, BoardSearch {
		// 쿼리 메소드
		// 제목에 키워드를 포함하는 board를 bno 기준으로 내림차순 정렬하여 페이징
    Page<Board> findByTitleContainingOrderByBnoDesc(String keyword, Pageable pageable);
}

Query

SQL과 유사한 JPQL구문을 지정하여 레포지토리 인터페이스에서 메서드와 쿼리를 매칭해준다.

  • JOIN과 같이 복잡한 쿼리를 실행
  • 원하는 속성들만 추출해서 Object[]로 처리하거나 DTO로 처리
  • nativeQuery속성값을 true로 지정하면 특정 데이터베이스에서 동작하는 SQL 사용 가능하다
    • nativeQuery: 실제 데이터베이스의 네이티브 SQL 쿼리를 실행할 때 사용된다. 엔티티 클래스의 필드명이 아닌 데이터베이스의 실제 컬럼명을 사용해야 한다. 엔티티 객체를 직접 반환하는 것이 아니라, Object[]DTO 등의 다른 형태로 결과를 매핑해야한다. 그러나 네이티브 쿼리를 사용할 때는 데이터베이스 종속성이 생기므로, JPQL을 활용하여 JPA가 제공하는 객체 지향적인 쿼리를 사용하는 것이 바람직하다.
package com.shop.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import com.shop.domain.Board;
import com.shop.repository.search.BoardSearch;

public interface BoardRepository extends JpaRepository<Board, Long>, BoardSearch {
	@Query("select b from Board b where b.title like concat('%', :keyword, '%')")
	Page<Board> findKeyword(String keyword, Pageable pageable);

	@Query(value = "select now()", nativeQuery=true)
	String getTime();
}
profile
상민

0개의 댓글