Home Repository Interface
Post
Cancel

Repository Interface

Repository Interface

Entity 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;
        
    private String name;
    private String email;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
  • Entity Annotation으로 객체를 Entity로 만들 수 있으며 Entity는 primary key가 필요하다.
  • @Id는 해당 프로퍼티가 테이블의 primary key역할을 한다고 나타내는 것이다.
  • @GeneratedValue는 PK의 값을 위한 자동 생성 전략을 명시한 것이다.
    • 선택적 속성으로 generator, strategy가 있다.
    • strategy
      • persistence provider가 PK를 생성할 때 사용해야 하는 PK생성 전략을 의미한다.
      • Default는 AUTO이며 IDENTITY, SEQUENCE, TABLE옵션이 있다.
        • AUTO : 특정 DB에 맞게 자동 선택
        • IDENTITY : DB의 identity컬럼 사용
        • SEQUENCE : DB의 시퀀스 컬럼 사용
        • TABLE : 유일성이 보장된 데이터베이스 테이블을 이용
    • generator
      • SequenceGenerator , TableGenerator Annotation에서 명시된 PK생성자를 재사용 할 때 사용한다.
  • 옵션 작성은 아래와 같이 할 수 있다.
1
2
@Id
@GeneratedValue(strategy=GenerationType.AUTO)

Repository 생성

  • Repositroy생성은 JpaRepository를 상속 받는 것으로 쉽게 생성 할 수 있다.
  • repository.UserRepository
1
2
3
// Entity Type, PK Type
public interface UserRepository extends JpaRepository<User, Long> {
}
  • 이렇게 interface를 만드는 것으로 save, findById, findAll, delete 같은 많은 기능을 사용 할 수 있다.
  • 상속 받을 때 JpaRepository에는 Entity Type과 PK Type을 작성 해야 한다.

JpaRepository method

  • findAll
    • 조건 없이 Table의 전체 값을 가져오는 method
    • 실제 서비스에서 성능 이슈로 잘 사용하지 않음
  • findAllById(Iterable<ID> ids)
    • id값을 list로 받아 조회 하는 mehtod
  • saveAll(Iterable<S> entities)
    • entity들을 받아 db에 한번에 저장하는 method
  • flush
    • jpa context에서 가지고 있는 DB값을 실제 DB에 반영하는 method
  • saveAndFlush(S entity)
    • save값을 jpa context에서 가지고 있지 않고 바로 DB에 저장
  • getOne(Id id)
    • 하나의 값 가져오기

CrudRepository

JpaRepository는 PagingAndSortingRepository를 상속 받고, PagingAndSortingRepository는 CrudRepository를 상속받았다. 실제 많이 사용하는 method는 대부분 CrudRepository에 정의 되어 있다.

  • save(S entity)
    • entity 저장
  • findById(Id id)
    • getOne과 비슷하지만 optional 객체로 매핑해서 리턴
  • existsById(Id id)
    • 객체의 존재 여부 확인
  • count()
    • 전체의 개수 반환
  • deleteById(Id id)
    • Id기준으로 지우기
  • delete(T entity)
    • 해당 entity지우기

JPA Repository 실습

DB 초기 데이터

  • data.sql 파일을 resources 하위에 생성하면 JPA가 로딩 할 때 해당 파일의 있는 Query를 한번 실행해준다.
  • Test에 사용하기 위해서는 test.resources를 만든 뒤 해당 resources하위에 생성하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
call next value for hibernate_sequence;
insert into user(`id`, `name`, `email`, `created_at`, `updated_at`) values(1, 'kms', 'kmslkh@naver.com', now(), now());

call next value for hibernate_sequence;
insert into user(`id`, `name`, `email`, `created_at`, `updated_at`) values(2, 'pjh', 'jjjh1616@naver.com', now(), now());

call next value for hibernate_sequence;
insert into user(`id`, `name`, `email`, `created_at`, `updated_at`) values(3, 'lyd', 'xcvdv@naver.com', now(), now());

call next value for hibernate_sequence;
insert into user(`id`, `name`, `email`, `created_at`, `updated_at`) values(4, 'tonny', 'tonny@nate.com', now(), now());

call next value for hibernate_sequence;
insert into user(`id`, `name`, `email`, `created_at`, `updated_at`) values(5, 'kms', 'sdkd@naver.com', now(), now());

→ Sequence “HIBERNATE_SEQUENCE” not found error

  • 해결
    • application에 defer-datasource-initialization을 true로 추가

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      
      spring:
      h2:
          console:
            enabled: true
      jpa:
          show-sql: true
          properties:
            hibernate:
              format_sql: true
          defer-datasource-initialization: true
      
  • 기본적으로 data.sql 스크립트가 Hibernate가 초기화 되기 이전에 실행되기 때문에 발생 된 문제였다. spring.jpa.defer-datasource-initialization을 true로 설정하는 것으로 해당 문제를 해결 할 수 있다. true로 설정하면 schema.sql 스크립트를 사용해 Hibernate에서 생성 한 스키마를 구축 후 data.sql을 통해 내용을 채울 수 있다.
  • 하지만 이렇게 데이터 베이스 초기화 기술을 혼합하는 것은 권장 되지 않는다.
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
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    void crud(){
        userRepository.save(new User());
        userRepository.findAll().forEach(System.out::println);
    }
}

### output
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        user
        (created_at, email, name, updated_at, id) 
    values
        (?, ?, ?, ?, ?)
Hibernate: 
    select
        user0_.id as id1_0_,
        user0_.created_at as created_2_0_,
        user0_.email as email3_0_,
        user0_.name as name4_0_,
        user0_.updated_at as updated_5_0_ 
    from
        user user0_
User(id=1, name=kms, email=kmslkh@naver.com, createdAt=2021-06-27T18:33:23.132097, updatedAt=2021-06-27T18:33:23.132097)
User(id=2, name=pjh, email=jjjh1616@naver.com, createdAt=2021-06-27T18:33:23.140096, updatedAt=2021-06-27T18:33:23.140096)
User(id=3, name=lyd, email=xcvdv@naver.com, createdAt=2021-06-27T18:33:23.141096, updatedAt=2021-06-27T18:33:23.141096)
User(id=4, name=tonny, email=tonny@nate.com, createdAt=2021-06-27T18:33:23.141096, updatedAt=2021-06-27T18:33:23.141096)
User(id=5, name=kms, email=sdkd@naver.com, createdAt=2021-06-27T18:33:23.141096, updatedAt=2021-06-27T18:33:23.141096)
User(id=6, name=null, email=null, createdAt=null, updatedAt=null)
  • DB Query를 보는것은 yml에서 설정 할 수 있다.

    1
    2
    3
    4
    5
    6
    
      spring:
        jpa:
          show-sql: true
          properties:
            hibernate:
              format_sql: true
    

save

1
2
3
4
5
6
7
8
9
10
11
12
13
@Transactional
@Override
public <S extends T> S save(S entity) {

    Assert.notNull(entity, "Entity must not be null.");

    if (entityInformation.isNew(entity)) { // isNew : getId가 null이라면 New로 본다.
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}
  • 위의 코드는 SimpleJpaRepository의 save코드이다.
  • Entity에 대한 Null체크 후 새로운 Entity라면 Persist(insert), 새로운 Entity가 아니라면 merge(update)를 수행한다.

findAll

  • 정렬 후 조회

    1
    
      userRepository.findAll(Sort.by(Direction.DESC, "name"))
    
  • ID list로 조회

    1
    
      userRepository.findAllById(Lists.newArrayList(1L, 3L, 5L));
    

getOne vs findById

  • getOne

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      @SpringBootTest
      class UserRepositoryTest {
    
          @Autowired
          private UserRepository userRepository;
        
          @Test
          void getOneTest(){
              User user = userRepository.getOne(1L);
              System.out.println(user);
          }
      }
    
    • 위의 test는 session error가 발생하는데 이는 getOne이 LazyFetch를 지원하기 때문이다.
    • @Transactional Annotation을 사용하면 위의 테스트를 정상적으로 실행 할 수 있다.
  • findById

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      @SpringBootTest
      class UserRepositoryTest {
    
          @Autowired
          private UserRepository userRepository;
        
          @Test
          void findByIdTest(){
              User user = userRepository.findById(1L).orElse(null);
              System.out.println(user);
          }
      }
    
    • findById는 eagerFetch를 지원한다.
    • findById는 반환이 Optional이기 때문에 별도의 처리(orElse)가 필요하다.
  • Lazy와 eager 차이

    • getOne의 구현을 보면 em(entity manager)에서 getReference로 참조만 하고 있고 실제 값을 구하는 시점에 세션을 통해 조회를 한다.
    • findById의 구현을 보면 em에서 바로 find를 entity 객체를 가져온다.

Delete

  • entity 삭제

    1
    2
    3
    4
    
      @Test
      void deleteEntityTest(){
          userRepository.delete(userRepository.findById(1L).orElseThrow(RuntimeException::new));
      }
    
    • 3개의 query가 실행된다. (Select 2, delete 1)
    • delete로 entity를 보내기위해 findById에서 1번, 해당 entity의 id를 가지고 다시한번 delete에서 entity있는지 확인할 때 1번, id를 가지고 지울 때 1번
  • id로 삭제

    1
    2
    3
    4
    
      @Test
      void DeleteByIdTest(){
          userRepository.deleteById(1L);
      }
    
    • 2개의 query가 실행된다.(select 1, delete 1)
    • 해당 id가 존재하는지 select 1번, 해당 Id로 delete 1번
  • deleteAll과 deleteAllInBatch

    • deleteAll의 내부 구현을 살펴보면 for문을 통해 삭제 할 entity의 개수만큼 delete Query가 실행되고, deleteAllInBatch는 Query를 만들어 한번에 삭제한다.

Paging

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
@SpringBootTest
class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    void crud(){
        Page<User> users = userRepository.findAll(PageRequest.of(1, 3)); 
        System.out.println("totalElements : " + users.getTotalElements());
        System.out.println("totalPages : " + users.getTotalPages());
        System.out.println("numberOfElements : " + users.getNumberOfElements());
        System.out.println("sort : " + users.getSort());
        System.out.println("size : " + users.getSize());

				users.getContent().forEach(System.out::println);
    }
}

### OUTPUT
totalElements : 5
totalPages : 2
numberOfElements : 2
sort : UNSORTED
size : 3
User(id=4, name=tonny, email=tonny@nate.com, createdAt=2021-06-27T19:45:44.787699, updatedAt=2021-06-27T19:45:44.787699)
User(id=5, name=kms, email=sdkd@naver.com, createdAt=2021-06-27T19:45:44.787699, updatedAt=2021-06-27T19:45:44.787699)
  • page당 size 3으로 paging해서 1번 Page가져오기
  • page번호는 0번부터 시작한다.

QBE(Query By Example)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.endsWith;

@SpringBootTest
class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    void crud(){
        ExampleMatcher matcher = ExampleMatcher.matching()
                .withIgnorePaths("name")
                .withMatcher("email", endsWith());
        Example<User> example = Example.of(new User("kms", "naver.com"), matcher);
        userRepository.findAll(example).forEach(System.out::println);

    }
}

###OUTPUT
User(id=1, name=kms, email=kmslkh@naver.com, createdAt=2021-06-27T19:53:32.292579, updatedAt=2021-06-27T19:53:32.292579)
User(id=2, name=pjh, email=jjjh1616@naver.com, createdAt=2021-06-27T19:53:32.301577, updatedAt=2021-06-27T19:53:32.301577)
User(id=3, name=lyd, email=xcvdv@naver.com, createdAt=2021-06-27T19:53:32.301577, updatedAt=2021-06-27T19:53:32.301577)
User(id=5, name=kms, email=sdkd@naver.com, createdAt=2021-06-27T19:53:32.302578, updatedAt=2021-06-27T19:53:32.302578)
  • example에 name과 email 조건을 넣었는데 withIgnorePaths로 이름은 무시되고, email의 경우 withMatcher로 endsWith()인지 확인한다.
  • 따라서 email이 naver.com으로 끝나는 모든 것을 보여준다.
  • matcher를 사용하지 않으면 name과 email이 완벽히 일치하는 것을 찾는다.
  • 다음과 같은 종류들이 있다.

    1
    2
    3
    4
    5
    6
    7
    
      ExampleMatcher.GenericPropertyMatchers.endsWith();
      ExampleMatcher.GenericPropertyMatchers.exact();
      ExampleMatcher.GenericPropertyMatchers.caseSensitive();
      ExampleMatcher.GenericPropertyMatchers.contains();
      ExampleMatcher.GenericPropertyMatchers.regex();
      ExampleMatcher.GenericPropertyMatchers.startsWith();
      ExampleMatcher.GenericPropertyMatchers.storeDefaultMatching();
    
This post is licensed under CC BY 4.0 by the author.