728x90
반응형
사용자 정의 레포지토리 구현
- 스프링 데이터 JPA 레포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성
- 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많다.
만약 인터페이스의 메소드를 직접 구현하고 싶다면
(1) JPA 직접 사용(EntityManager
)
(2) 스프링 JDBC Template 사용
(3) MyBatis 사용
(4) 데이터베이스 커넥션 직접 사용
(5) QueryDSL 사용
을 해야한다.
사용자 정의 인터페이스
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
사용자 정의 구현 클래스
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m", Member.class).getResultList();
}
}
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
...
}
- 명명 규칙(
레포지토리 인터페이스 이름 + Impl
또는사용자 정의 인터페이스 이름 + Impl
)에 맞게 클래스를 생성하면 스프링 데이터 JPA가 인식하여 스프링 빈으로 등록한다.
※사용자 정의 인터페이스 이름 + Impl
이 가능해지면서 사용자 정의 인터페이스 구현 클래스 명을 인터페이스 이름과 동일하게 할 수 있어 더 직관적이며 추가로 여러 인터페이스를 분리해서 구현하는 것도 가능하다. - Impl 대신 다른 이름으로 변경하고 싶다면 XML 또는 JavaConfig 설정을 통해 변경 가능하다.
- XML 설정
- JavaConfig 설정<repositories base-package="study.datajpa.repository" repository-impl-postfix="Impl" />
@EnableJpaRepositories(basePackages = "study.datajpa.repository", repositoryImplementationPostfix = "Impl")
※ 참고로 실무에서는 주로 QueryDSL이나 SpringJdbcTemplate를 함께 사용할 때 사용자 정의 레포지토리를 구현한다.
※ 참고로 항상 사용자 정의 레포지토리가 필요한 것은 아니다. 그냥 임의의 레포지토리 클래스를 만들어 스프링 빈으로 등록하여 사용해도 된다. 이 경우엔 스프링 데이터 JPA와는 무관하게 별도로 동작한다.
Auditing
엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하는 기능
순수 JPA 사용
@MappedSuperclass
@Getter
public class JpaBaseEntity {
@Column(updatable = false)
private LocalDateTime createDate;
private LocalDateTime updateDate;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
this.createDate = now;
this.updateDate = now;
}
@PreUpdate
public void preUpdate() {
LocalDateTime now = LocalDateTime.now();
this.updateDate = now;
}
}
public class Member extends JpaBaseEntity {
...
}
@MappedSuperclass
: 실제 상속관계가 아닌 속성만 상속관계를 갖는 것@PrePersist
: INSERT 실행 전 동작@PreUpdate
: UPDATE 실행 전 동작@PreDelete
: DELETE 실행 전 동작@PostPersist
: INSERT 실행 후 동작@PreUpdate
: UPDATE 실행 후 동작@PreDelete
: DELETE 실행 후 동작@PreLoad
: SELECT 실행 후 동작
테스트 코드
@Test
public void jpaEventBaseEntity() throws Exception {
// given
Member member = new Member("member1");
memberJpaRepository.save(member);
Thread.sleep(100);
member.setUsername("member2");
em.flush();
em.clear();
// when
Member findMember = memberJpaRepository.find(member.getId());
// then
System.out.println("findMember.createDate" + findMember.getCreateDate());
System.out.println("findMember.updateDate" + findMember.getUpdateDate());
}
테스트 결과
스프링 데이터 JPA 사용
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
@Bean
public AuditorAware<String> auditorProvider() {
// 실무에서는 등록자, 수정자를 세션 정보나 스프링 시큐리티 로그인 정보에서 ID를 받아 사용
return () -> Optional.of(UUID.randomUUID().toString());
}
}
@EntityListeners(value = AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createDate;
@LastModifiedDate
private LocalDateTime updateDate;
@CreatedBy
@Column(updatable = false)
private String createBy;
@LastModifiedBy
private String updateBy;
}
- 스프링 부트 설정 클래스에
@EnableJpaAuditing
어노테이션 설정을 한다. - 저장시점에 등록일, 등록자, 수정일, 수정자 정보가 같이 저장된다. 이렇게 해야 마지막 수정자를 찾기 편리하다. 만약 저장시 수정 정보를 저장하고 싶지 않으면
@EnableJpaAuditing(modifyOnCreate = false)
으로 설정하면 된다. - 실무에서는 등록자, 수정자를 세션 정보나 스프링 시큐리티 로그인 정보을 받아 사용한다.
- 등록/수정일, 등록/수정자 엔티티를 분리하여 사용해도 좋다. 대부분의 엔티티에는 등록/수정일은 필수로 사용되지만 등록/수정자는 사용되지 않는 경우도 있어 부모 BaseTimeEntity를 구현하고 자식 BaseEntity를 구현하여 사용해도 된다. (부모 : 등록/수정일, 자식 : 등록/수정자)
테스트 코드
@Test
public void eventBaseEntity() throws Exception {
// given
Member member = new Member("member1");
memberRepository.save(member);
Thread.sleep(100);
member.setUsername("member2");
em.flush();
em.clear();
// when
Member findMember = memberRepository.findById(member.getId()).get();
// then
System.out.println("findMember.createDate = " + findMember.getCreateDate());
System.out.println("findMember.updateDate = " + findMember.getUpdateDate());
System.out.println("findMember.createBy = " + findMember.getCreateBy());
System.out.println("findMember.updateBy = " + findMember.getUpdateBy());
}
테스트 결과
Web 확장 - 도메인 클래스 컨버터
HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아 바인딩 해주는 기능
도메인 클래스 컨버터 사용 전
@GetMapping(value="/members/{id}")
public String findMember(@PathVariable("id") Long id) {
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
- 파라미터로 받은 ID로 엔티티 조회 후 해당 엔티티의
username
을 반환
도메인 클래스 컨버터 사용 후
@GetMapping(value="/members/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
- 파라미터로 ID 값을 받지만 중간에서 도메인 클래스 컨버터가 해당 ID를 가진 엔티티를 바인딩하기 때문에 바로 엔티티의
username
을 반환한다. - 도메인 클래스 컨버터도 레포지토리를 사용하여 엔티티를 찾는다.
※ 주의할 점으로 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면 이 엔티티는 조회용으로만 사용해야 한다. 왜냐하면 트랜잭션이 없는 범위에서 조회된 엔티티이기 때문에 엔티티를 변경해도 DB에 반영되지 않기 때문이다.
Web 확장 - 페이징과 정렬
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있다.
페이징과 정렬 예제
@GetMapping(value="/members")
public Page<Member> list(Pageable pageable) {
return memberRepository.findAll(pageable);
}
- 리턴타입을
Page<T>
로 하고 파라미터로 Pageable 인터페이스를 받아 페이징과 정렬을 구현할 수 있다. - Pageable은 인터페이스로 실제는
org.springframework.data.domain.PageRequest
객체를 생성한다.
페이징과 정렬 예제 결과
요청 파라미터
- page : 현재 페이지로 0부터 시작한다.
- size : 한 페이지에 노출할 데이터 건수를 지정한다.
- sort : 정렬 조건을 지정한다. sort 파라미터를 추가하여 정렬을 추가할 수 있다. (ex.
sort=id, desc&sort=username
)
기본값 설정
전역설정 (application.yml)
spring:
data:
web:
pageable:
default-page-size: 10 /# 기본 페이지 사이즈 /
max-page-size: 1000 /# 최대 페이지 사이즈 /
개별설정 (@PageableDefault)
@GetMapping(value="/members")
public Page<Member> list(@PageableDefault(size = 3
, sort = "username"
, direction = Direction.DESC) Pageable pageable) {
return memberRepository.findAll(pageable);
}
접두사
- 페이징 정보가 둘 이상이면
@Qualifier
을 사용하여 접두사를 추가하여 구분한다. (ex.{접두사명}_xxx
)
public String list( @Qualifier("member") Pageable memberPageable, @Qualifier("order") Pageable orderPageable, ...
Page 내용을 DTO로 변환하기
엔티티를 API로 노출하면 다양한 문제가 발생하기 때문에 반드시 DTO로 변환하여 반환해야 한다.
@Data
public class MemberDto {
private Long id;
private String username;
private String teamName;
public MemberDto(Long id, String username, String teamName) {
this.id = id;
this.username = username;
this.teamName = teamName;
}
public MemberDto(Member m) {
this.id = m.getId();
this.username = m.getUsername();
}
}
@GetMapping(value="/memberdtos")
public Page<MemberDto> listDto(@PageableDefault(size = 3
, sort = "username"
, direction = Direction.DESC) Pageable pageable) {
return memberRepository.findAll(pageable).map(MemberDto::new);
}
- 페이징과 정렬도 구현해야 할 경우 Page는
map()
을 지원하기 때문에 내부 데이터를 다른 것으로 변경할 수 있어map()
을 사용하여 구현할 수 있다.
Page를 1부터 시작하기
스프링 데이터는 Page 인덱스가 0부터 시작한다. 1부터 시작하도록 변경해보자
- Page, Pageable을 파라미터로 사용하지 않고 직접 클래스로 만들어 커슽터마이징하여 처리하고, 직접 PageRequest(Pageable 구현체)를 생성하여 레포지토리에 넘겨 사용한다. 또한 응답 Page도 직접 만들어서 사용해야 한다.
application.yml
에spring.data.web.pageable.one-indexed-parameters = true
로 설정한다. 하지만 이 방법은 Page 파라미터를 -1 할 뿐이기 때문에 0, 1페이지 응답 Page에서 모두 0페이지 인덱스를 사용하는 한계가 존재한다.
spring: data: web: pageable: one-indexed-parameters: true
결과적으로 위 2가지 방법을 사용하면 1 페이지부터 사용이 가능하지만 한계가 있어 가장 좋은 방법은 0 페이지부터 사용하는 것이다.
728x90
반응형
'Dev > Spring Data JPA' 카테고리의 다른 글
[Spring Data JPA] 나머지 기능들 (0) | 2021.12.19 |
---|---|
[Spring Data JPA] 스프링 데이터 JPA 분석 (0) | 2021.12.14 |
[Spring Data JPA] 쿼리 메소드 기능 (0) | 2021.12.09 |
[Spring Data JPA] 공통 인터페이스 기능 (0) | 2021.11.29 |
[Spring Data JPA] 예제 도메인 모델 (0) | 2021.11.29 |
댓글