[JPA]2. 활용

스프링 데이터는 여러개의 프로젝트로 되어 있다

서브 프로젝트명 내용
스프링 데이터 SQL & NoSQL 저장소 지원 프로젝트의 묶음.
스프링 데이터 Common 여러 저장소 지원 프로젝트의 공통 기능 제공.
스프링 데이터 REST 저장소의 데이터를 하이퍼미디어 기반 HTTP 리소스로(REST API로) 제공하는 프로젝트.
스프링 데이터 JPA 스프링 데이터 Common이 제공하는 기능에 JPA 관련 기능 추가.
  • 스프링 데이터는 공통기능인 common을 바탕으로 각 저장소에 따른 서브 프로젝트가 존재한다.
  • 스프링 데이터 JPA는 common에 JPA 관련 기능을 추가한 것이기에 common을 먼저 공부해야한다.

1.Spring Data Common : Repository

앞 아티클에서 제네릭한 Repository를 만드는 가장 진보적인 방법이라고 했던 코드가 있다

제네릭한 Repo를 만드는 가장 진보적인 형태의 방법, @Repository 조차 없음
1
2
3
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository2 extends JpaRepository<Post, Long> {
}
  • 이 JpaRepository 인터페이스는 PagingAndSortingRepository 를 상속하고 있다

  • PagingAndSortingRepository는 CrudRepository를 상속하고 있다

  • CrudRepository는 Repository를 상속하고 있다

  • 즉 계층 구조로 치면 JpaRepository-PagingAndSortingRepository-CrudRepository-Repository로 되어 있다

  • JpaRepository 인터페이스는 Spring Data JPA에 속하는 인터페이스며 상위 3개의 인터페이스는 Spring Data Common에 속하는 인터페이스다

  • 상세

    • Repository
      • Repository 인터페이스는 메소드가 없는 마커 인터페이스의 역할을 한다
    • CrudRepository
CrudRepository.java 기본적인 crud기능이 정의되어 있다
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/*
* 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 java.util.Optional;

/**
* Interface for generic CRUD operations on a repository for a specific type.
*
* @author Oliver Gierke
* @author Eberhard Wolff
*/

//스프랑에서 자동으로 이 인터페이스 빈을 등록하지 않도록
//중간단계 인터페이스들에게 다 확인할 수 있
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {

/**
* 주어진 엔티티를 저장하고 그 인스턴스를 다시 반환한다
*
* @param entity must not be {@literal null}.
* @return the saved entity will never be {@literal null}.
*/
<S extends T> S save(S entity);

/**
* 여러 인스턴스를 한번에 저장한다
* Iterable 형태의 인스턴스를 사용하며 역시 해당 인스턴스를 반환한다
*
* @param entities must not be {@literal null}.
* @return the saved entities will never be {@literal null}.
* @throws IllegalArgumentException in case the given entity is {@literal null}.
*/
<S extends T> Iterable<S> saveAll(Iterable<S> entities);

/**
* Java8부터 지원하는 Optional을 사용한다
* id로 검색해서 찾을 수 있다
*
* @param id must not be {@literal null}.
* @return the entity with the given id or {@literal Optional#empty()} if none found
* @throws IllegalArgumentException if {@code id} is {@literal null}.
*/
Optional<T> findById(ID id);

/**
* 있는지 없는지만 확인
*
* @param id must not be {@literal null}.
* @return {@literal true} if an entity with the given id exists, {@literal false} otherwise.
* @throws IllegalArgumentException if {@code id} is {@literal null}.
*/
boolean existsById(ID id);

/**
* 사실 테스트에서만 써야한다. 해당 엔티티의 데이타를 전부 가져온다
*
* @return all entities
*/
Iterable<T> findAll();

/**
* Returns all instances of the type with the given IDs.
*
* @param ids
* @return
*/
Iterable<T> findAllById(Iterable<ID> ids);

/**
* Returns the number of entities available.
*
* @return the number of entities
*/
long count();

/**
* Deletes the entity with the given id.
*
* @param id must not be {@literal null}.
* @throws IllegalArgumentException in case the given {@code id} is {@literal null}
*/
void deleteById(ID id);

/**
* Deletes a given entity.
*
* @param entity
* @throws IllegalArgumentException in case the given entity is {@literal null}.
*/
void delete(T entity);

/**
* Deletes the given entities.
*
* @param entities
* @throws IllegalArgumentException in case the given {@link Iterable} is {@literal null}.
*/
void deleteAll(Iterable<? extends T> entities);

/**
* Deletes all entities managed by the repository.
*/
void deleteAll();
}
  • 기능을 정의한 최상단 인터페이스로 Repository 인터페이스를 상속하고 있다
  • 위에서 보다시피 기본적인 CRUD 메소드를 제공한다
  • 이 CRUD 기능을 이용한 테스트 샘플을 작성해 본다
    테스트용 h2 의존성 추가
    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
    </dependency>
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
package me.rkaehdaos.jpademo1;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;//AssertJ용이 좋다

@RunWith(SpringRunner.class)
@DataJpaTest //Data Access Layer만 슬라이싱 테스트
public class PostRepository2Test {

@Autowired
PostRepository2 postRepository2;

@Test
public void crudRepository() {
//Given
Post post = new Post();
post.setTitle("hello spring boot common");
assertThat(post.getId()).isNull();
// When
Post newPost = postRepository2.save(post);
assertThat(newPost.getId()).isNotNull();
// Then
}
}
  • 사실 원래 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
        */
        @NoRepositoryBean
        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);

2.Spring Data Common : Repository 인터페이스 정의

  • 지금까지 Repository 생성 방법은 Spring Data JPA 혹은 Spring Data Common에서 제공하는 인터페이스를 직접 상속받아 만들었다

  • 이게 좋다 사실 . 많은 기능이 한꺼번에 들어오기 때문에

  • 만약 내가 정의하고 싶은 기능만 정의해서 Repository를 생성하고 싶다면?

  • @RepositoryDefinition을 사용하면 된다

    1
    2
    3
    4
    5
    @RepositoryDefinition(domainClass = Comment.class, idClass = Long.class)
    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
    @NoRepositoryBean// 이 인터페이스는 빈이 될 필요가 없다
    //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
    3
    public 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
    @RunWith(SpringRunner.class)
    @DataJpaTest
    public class CommentRepositoryTest {

    @Autowired // 빈주입
    CommentRepository commentRepository;

    @Test
    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
      @NoRepositoryBean
      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); //메소드 레퍼런스로 다 간결하게 가능
  • 콜렉션은 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
      • 선언된 쿼리를 찾고 없는 경우 메소드 이름 분석해서 쿼리 생성(기본값)
  • 쿼리 생성 전략 세팅

    • @EnableJpaRepositories 에서 설정가능
      기본값이 CREATE_IF_NOT_FOUND이므로 밑에처럼 해줄 필요는 없다. 예시임
      1
      2
      3
      4
      5
      6
      7
      @SpringBootApplication
      @EnableJpaRepositories(queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
      public class Jpademo1Application {
      public static void main(String[] args) {
      SpringApplication.run(Jpademo1Application.class, args);
      }
      }
  • 쿼리 만드는 방법

    • 구성: 리턴타입 {접두어}{도입부}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
쿼리만드는 메소드 정의, IDE에 따라 자동완성이 지원되는 듯 하다
1
2
3
4
5
6
7
8
9
public interface CommentRepositoryextends MyRepository<Comment, Long> {
//MyRepository에 커스텀 정의된 부분도 include되어있다

List<Comment> findByCommentContains(String keyword);

//페이징으로 받고 싶으면 Pageable 파라미터를 주어야 한다
//List<Comment>로 받을 수 있지만 페이지 관련 정보가 누락된다
Page<Comment> findByLikeCountGreaterThanAndPostOrderByCreatedDesc(int likeCount, Post post, Pageable pageable);
}
쿼리 테스트
1
2
3
4
5
6
7
8
9
@Test
public void crud() {
// /*
Comment comment = new Comment();
comment.setComment("Hello spring data jpa Comment");
commentRepository.save(comment);
List<Comment> comments = commentRepository.findByCommentContains("spring");
assertThat(comments.size()).isEqualTo(1);
}
  • 계속적으로 확장이 가능하다
  • 다음은 3개를 받아서 likecount 10보다 큰 2개를 오름차순으로 정리, 가장 첫번째 것의 값을 assertThat한 것이다
  • 정열을 Sort를 따로 줄 수도 있지만 이처럼 메소드에 OrderBy로 처리하는 것이
    좀더 직관적인 듯 하다
CommentRepository.java
1
2
3
4
5
public interface CommentRepository extends MyRepository<Comment, Long> {
//MyRepository에 커스텀 정의 include

List<Comment> findByCommentContainsIgnoreCaseAndLikeCountGreaterThanOrderByLikeCountAsc(String keyword, int likeCount);

CommentRepositoryTest.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
@RunWith(SpringRunner.class)
@DataJpaTest
public class CommentRepositoryTest {

@Autowired // 빈주입
CommentRepository commentRepository;

@Test
public void crud() {
// /*
this.createComment(100,"spring data jpa");
this.createComment(55,"HIBERNATE SPRING");
this.createComment(1,"small one");

List<Comment> comments =
commentRepository
.findByCommentContainsIgnoreCaseAndLikeCountGreaterThanOrderByLikeCountAsc("Spring",10);
assertThat(comments.size()).isEqualTo(2);
assertThat(comments).first().hasFieldOrPropertyWithValue("likeCount",55);

}
private void createComment(int likeCount, String commentSentense) {
Comment comment = new Comment();
comment.setComment(commentSentense);
comment.setLikeCount(likeCount);
commentRepository.save(comment);
}
}
  • 페이지 쿼리
    • 페이지쿼리는 Pageable 파라미터가 필요하다고 했다
    • 또한 Page<>로 받아야 페이지 정보가 유용하다
      Pageable 인자를 추가하고 리턴을 Page<>형태로 바꿨다
      1
      2
      3
      4
      5
      6
      public interface CommentRepository extends MyRepository<Comment, Long> {
      //MyRepository에 커스텀 정의 include

      //List<Comment> findByCommentContainsIgnoreCaseAndLikeCountGreaterThanOrderByLikeCountAsc(String keyword, int likeCount);
      Page<Comment> findByCommentContainsIgnoreCase(String keyword, Pageable pageable);
      }
테스트 코드
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
@Test
public void crud() {
// /*
this.createComment(100, "spring data jpa");
this.createComment(55, "HIBERNATE SPRING");
this.createComment(1, "small one");

//0부터 10개씩 likeCount에 대해서 내림차순
PageRequest pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "LikeCount"));

Page<Comment> comments = commentRepository.findByCommentContainsIgnoreCase("Spring", pageRequest);
assertThat(comments.getNumberOfElements()).isEqualTo(2);
//Page->Slice->Streamable->Iterable, Iterable이므로 first 접근가능
assertThat(comments).first().hasFieldOrPropertyWithValue("likeCount", 100);


}

private void createComment(int likeCount, String commentSentense) {
Comment comment = new Comment();
comment.setComment(commentSentense);
comment.setLikeCount(likeCount);
commentRepository.save(comment);
}

  • 스트리밍
    • 자바 8부터 있는 Stream<>으로 리턴 받을 수 있다
    • Stream을 다 쓴 다음 close()하여야 한다
    • 아니면 자바8의 try-with-resource를 쓰면 좋다

Related POST

공유하기