서브 프로젝트명 | 내용 |
---|---|
스프링 데이터 | SQL & NoSQL 저장소 지원 프로젝트의 묶음. |
스프링 데이터 Common | 여러 저장소 지원 프로젝트의 공통 기능 제공. |
스프링 데이터 REST | 저장소의 데이터를 하이퍼미디어 기반 HTTP 리소스로(REST API로) 제공하는 프로젝트. |
스프링 데이터 JPA | 스프링 데이터 Common이 제공하는 기능에 JPA 관련 기능 추가. |
- 스프링 데이터는 공통기능인 common을 바탕으로 각 저장소에 따른 서브 프로젝트가 존재한다.
- 스프링 데이터 JPA는 common에 JPA 관련 기능을 추가한 것이기에 common을 먼저 공부해야한다.
1.Spring Data Common : Repository
앞 아티클에서 제네릭한 Repository를 만드는 가장 진보적인 방법이라고 했던 코드가 있다
1 | import org.springframework.data.jpa.repository.JpaRepository; |
이 JpaRepository 인터페이스는 PagingAndSortingRepository 를 상속하고 있다
PagingAndSortingRepository는 CrudRepository를 상속하고 있다
CrudRepository는 Repository를 상속하고 있다
즉 계층 구조로 치면 JpaRepository-PagingAndSortingRepository-CrudRepository-Repository로 되어 있다
JpaRepository 인터페이스는 Spring Data JPA에 속하는 인터페이스며 상위 3개의 인터페이스는 Spring Data Common에 속하는 인터페이스다
상세
- Repository
- Repository 인터페이스는 메소드가 없는 마커 인터페이스의 역할을 한다
- CrudRepository
- Repository
1 | /* |
- 기능을 정의한 최상단 인터페이스로 Repository 인터페이스를 상속하고 있다
- 위에서 보다시피 기본적인 CRUD 메소드를 제공한다
- 이 CRUD 기능을 이용한 테스트 샘플을 작성해 본다
테스트용 h2 의존성 추가 1
2
3
4
5<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
1 | package me.rkaehdaos.jpademo1; |
사실 원래 PostRepository2는 JpaRepository를 상속받고 따로 작성한 코드가 하나도 없기 때문에 원래 테스트가 필요한 코드가 아니다. 어디까지나 학습용 테스트임을 감안
테스트는 성공한다
특이점! 쿼리를 보면 insert 쿼리가 없다는 것을 알 수 있다. 왜!?
- @DataJpaTest 정의를 보면 @Transactional이 붙어 있다
- @Transactional이 붙어 있으면 테스트로 바뀐 부분이 기본적으로 롤백을 하게 된다
이것은 스프링 프레임워크의 기본 - 하이버네이트는 필요할때 DB에 데이타를 싱크하는 성질을 가지고 있다
- 따라서 하이버네이트는 어차피 롤백하는 쿼리이기 때문에 insert 자체를 하지 않는 것
- 스프링 프레임워크와 하이버네이트의 특성의 오묘한 조합으로 이러한 결과가 나옴
- select문이 있는것은 getId()를 통해 id가 필요한 것은 알기 때문
- 꼭 insert를 하기 위해서는 테스트 메소드에
@Rollback(false)
를 해주면 롤백 설정이 false가 되서 무조건 커밋이 되므로 insert문이 실행됨을 확인할 수 있다
상세
- PagingAndSortingRepository
- PagingAndSortingRepository는 기본적으로 페이징 기능을 제공한다
PagingAndSortingRepository.java 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49/*
* Copyright 2008-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
/**
* Extension of {@link CrudRepository} to provide additional methods to retrieve entities using the pagination and
* sorting abstraction.
*
* @author Oliver Gierke
* @see Sort
* @see Pageable
* @see Page
*/
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
/**
* Returns all entities sorted by the given options.
*
* @param sort
* @return all entities sorted by the given options
*/
Iterable<T> findAll(Sort sort);
/**
* Returns a {@link Page} of entities meeting the paging restriction provided in the {@code Pageable} object.
*
* @param pageable
* @return a page of entities
*/
Page<T> findAll(Pageable pageable);
} - 페이징 기능을 쓸떄는 Pageable 파라미터를 주게 된다
pageable테스트 1
2
3
4
5
6
7
8
9
10
11
12
13//Pageable 테스트
// 리턴타입이 Page임을 주목
// 팩토리 패턴 of 주목
Page<Post> postPage = postRepository2.findAll(PageRequest.of(0, 10));
//전체 element의 갯수는 하나
assertThat(postPage.getTotalElements()).isEqualTo(1);
//현재 page 넘버는 0
assertThat(postPage.getNumber()).isEqualTo(0);
//현재 사이즈는 위에서 요청한 사이즈 10개
assertThat(postPage.getSize()).isEqualTo(10);
//현재 페이지에 들어올 수 있는 개수 인듯
assertThat(postPage.getNumberOfElements()).isEqualTo(1);
- PagingAndSortingRepository는 기본적으로 페이징 기능을 제공한다
- PagingAndSortingRepository
2.Spring Data Common : Repository 인터페이스 정의
지금까지 Repository 생성 방법은 Spring Data JPA 혹은 Spring Data Common에서 제공하는 인터페이스를 직접 상속받아 만들었다
이게 좋다 사실 . 많은 기능이 한꺼번에 들어오기 때문에
만약 내가 정의하고 싶은 기능만 정의해서 Repository를 생성하고 싶다면?
@RepositoryDefinition을 사용하면 된다
1
2
3
4
5
public interface CommentRepository{
Comment save(Comment comment);
List<Comment> findAll();
}도메인 클래스와 id클래스를 정해준다
필요한 기능은 정의한다. 저정도는 스프링 Data에서 알아서 만들어 준다
이렇게 하면 직접 정의한 기능만 들어 있는 Repository가 완성 된다
대신 이렇게 되면 해당 메소드들은 개발자가 구현한 것이므로 테스트 코드가 필요하다
또한 레포지토리 10개가 전부 save를 가지고 있다면? save 코드 10개?
이러할때는 해당 메소드들이 구현된 @NoRepositoryBean 커스텀 인터페이스를 만들고
빈등록될 인터페이스에서 해당 커스텀 인터페이스를 상속받게 하면 된다
해당 커스텀 인터페이스는 제네릭으로 타입을 처리하도록 한다MyRepository.java 커스텀 인터페이스, 사용자 코드 정의가 들어 있다 1
2
3
4
5
6
7// 이 인터페이스는 빈이 될 필요가 없다
//Serializable 할 수 있는 Id를 사용해야 한다
public interface MyRepository<T, Id extends Serializable> extends Repository<T, Id> {
//필요한 메소드를 정의
<E extends T> E save(E comment);
List<T> findAll();
}실제 빈이 되는 CommentRepository, 커스텀 인터페이스를 상속받았다 1
2
3public interface CommentRepository<T, Id> extends MyRepository<Comment, Long> {
//이러면 MyRepository에 커스텀 정의된 부분만 들어간다
}CommentRepositoryTest.java, CommentRepository의 테스트 클래스다 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CommentRepositoryTest {
// 빈주입
CommentRepository commentRepository;
public void crud() {
Comment comment = new Comment();
comment.setComment("Hello Comment");
commentRepository.save(comment);
List<Comment> all = commentRepository.findAll();
assertThat(all.size()).isEqualTo(1);
assertThat(commentRepository.count()).isEqualTo(1);
}
}여러모로 번거롭다 그냥 원래 방법을 쓰고 이거는 이러것이 있다 정도로만
3.Spring Data Common : Null 처리
Spring Data 2.0부터 자바 8의 Optional을 지원
- Ex) Optional
findById(Long id); MyRepository.java 커스텀 인터페이스에 위에 내용을 추가한다 1
2
3
4
5
6
7
public interface MyRepository<T, Id extends Serializable> extends Repository<T, Id> {
//필요 메소드 정의
<E extends T> E save(E comment);
List<T> findAll();
<E extends T>Optional<E> findById(Id id); //추가
}Optional Test, null체크를 안해도 된다 1
2
3
4
5
6
7//Optional Test
Optional<Comment> byId = commentRepository.findById(100l);
assertThat(byId).isEmpty(); //Optional 객체 안이 실제로는 비어있다
Comment comment2 = byId.orElse(new Comment()); //값이 비면 새 객체로 할 수도 있다
assertThat(comment2).isNotNull();
//Comment comment3 = byId.orElseThrow(()->new IllegalArgumentException()); //예외를 던질 수도 있다
//Comment comment4 = byId.orElseThrow(IllegalArgumentException::new); //메소드 레퍼런스로 다 간결하게 가능
- Ex) Optional
콜렉션은 Null을 지원하지 않고 , 비어있는(Empty) 콜렉션을 리턴한다
스프링 프레임워크 5.0 부터 지원하는 Null 어노테이션
@NotNullApi, @NonNull, @Nullable
런타임 체크 지원
JSR 305 어노테이션을 메타 어노테이션으로 가지고 있음
인텔리J에서 add Runtime asserttion 옵션에서 스프링 어노테이션 추가해서 사용 가능
3.Spring Data Common : Query 만들기
쿼리 생성 전략
- CREATE
- 메소드 이름을 분석해서 쿼리를 만드는 방법 - Spring Data가 쿼리를 만들어 준다
- USE_DECLARED_QUERY
- 메소드 이름이 아닌 부가 정보(@Query 등)를 바탕으로 쿼리를 찾아 실행
- CREATE_IF_NOT_FOUND
- 선언된 쿼리를 찾고 없는 경우 메소드 이름 분석해서 쿼리 생성(기본값)
- CREATE
쿼리 생성 전략 세팅
- @EnableJpaRepositories 에서 설정가능
기본값이 CREATE_IF_NOT_FOUND이므로 밑에처럼 해줄 필요는 없다. 예시임 1
2
3
4
5
6
7
public class Jpademo1Application {
public static void main(String[] args) {
SpringApplication.run(Jpademo1Application.class, args);
}
}
- @EnableJpaRepositories 에서 설정가능
쿼리 만드는 방법
- 구성: 리턴타입 {접두어}{도입부}By{프로퍼티표현식}(조건식)[(And|Pr){프로퍼티표현식}(조건식)]{정렬조건}(매개변수)
- 리턴타입: List
, T, Optional , Page , Slice , Stream - 접두어: Find, Get, Query, Count, …
- 도입부: Distinct, First(N), Top(N)
- 프로퍼티 표현식: Person.Address.ZipCode => find(Person)ByAddress_ZipCode(..)
- 조건식: IgnoreCase, Between, LessThan, GreaterThan, Like, Contains, …
- 정렬조건: Orderby{프로퍼티}Asc|Desc
1 | public interface CommentRepositoryextends MyRepository<Comment, Long> { |
1 |
|
- 계속적으로 확장이 가능하다
- 다음은 3개를 받아서 likecount 10보다 큰 2개를 오름차순으로 정리, 가장 첫번째 것의 값을 assertThat한 것이다
- 정열을 Sort를 따로 줄 수도 있지만 이처럼 메소드에 OrderBy로 처리하는 것이
좀더 직관적인 듯 하다
1 | public interface CommentRepository extends MyRepository<Comment, Long> { |
1 |
|
- 페이지 쿼리
- 페이지쿼리는 Pageable 파라미터가 필요하다고 했다
- 또한 Page<>로 받아야 페이지 정보가 유용하다
Pageable 인자를 추가하고 리턴을 Page<>형태로 바꿨다 1
2
3
4
5
6public interface CommentRepository extends MyRepository<Comment, Long> {
//MyRepository에 커스텀 정의 include
//List<Comment> findByCommentContainsIgnoreCaseAndLikeCountGreaterThanOrderByLikeCountAsc(String keyword, int likeCount);
Page<Comment> findByCommentContainsIgnoreCase(String keyword, Pageable pageable);
}
1 |
|
- 스트리밍
- 자바 8부터 있는 Stream<>으로 리턴 받을 수 있다
- Stream을 다 쓴 다음 close()하여야 한다
- 아니면 자바8의 try-with-resource를 쓰면 좋다