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();