자바 웹 개발 워크북 - 예스24
아래내용은 ‘자바 웹 개발 워크북(구멍가게 코딩단)’ 책의 pp. 432 ~ 508 (5.3. Spring Data JPA ~ 5.4. 게시물 관리 완성하기)의 내용을 토대로 작성되었습니다.
엔티티의 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는 다음과 같은 메서드를 기본으로 지원한다.
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()
: 테이블의 엔티티 총 개수 확인PageRequest.of(페이지 번호, 페이지 수, 정렬조건)
: 조건에 맞는 Pageable 객체를 반환하여 findAll에 들어가 페이징 조건을 생성Sort.by(컬럼).속성
을 사용하여 컬럼을 기준으로 정렬할 수 있다.Pageable
: 페이징 조건을 저장하는 객체Page<Entity>
: 페이징 조건에 따른 엔티티를 저장하는 객체getTotalElements()
: 테이블의 총 엔티티 개수getTotalPages()
: 해당 페이징 조건의 페이지 수에에 따른 테이블의 페이지 개수. 예컨대 총 엔티티가 99개이고, 페이징 조건이 10개씩이였다면 총 페이지 개수는 10개이고, 마지막 페이지에는 9개가 들어간다.getNumber()
: 현재 페이지 번호. 0부터 시작. PageRequest.of
에서 설정한 페이지 번호getSize()
: 페이지 수. PageRequest.of
에서 설정한 페이지 수getContet()
: 현재 페이지에 포함된 엔티티를 List<Entity>
로 반환. 만약 페이지 수가 10, 총 엔티티가 99라면, 마지막 페이지는 9개의 엔티티만 반환된다.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);
}
SQL과 유사한 JPQL구문을 지정하여 레포지토리 인터페이스에서 메서드와 쿼리를 매칭해준다.
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();
}