목차 (클릭시 해당 목차로 이동)
사용자 정의 리포지토리 구현
Data JPA를 사용하다보면 인터페이스의 메소드를 직접 구현해야할 때가 있습니다.
- JPA 직접 사용(EntityManager)
- JDBC Template 사용
- MyBatis 사용
- 데이터베이스 커넥션 직접사용
- QueryDSL 사용
이때 Data JPA의 인터페이스를 직접 구현하는 것은 너무 많기때문에 Data JPA에서 사용자 정의 리포지토리 구현 기능을 제공합니다.
사용자 정의 리포지토리 구현 기능 사용법
- 직접 구현할 메소드가 있는 인터페이스 (아무이름) (ex. MemberRepositoryCustom) 을 만듭니다.
- 구현클래스 MemberRepository...Impl 로 이름을 짓고 MemberRepositoryCustom를 상속받아 메소드를 구현합니다.
- MemberRepository 클래스에서 JpaRepository와 함께 MemberRepositoryCustom을 상속받습니다.
그러면 MemberRepository에서 구현클래스에서 구현한 메소드를 사용할 수 있습니다.
자바여서 가능한 것이 아니라 Data JPA에서 해주는 것입니다.
구현 클래스의 이름은 MemberRepository ... Impl 이것만 맞춰주면 됩니다.
MemberRepositoryCustom
package study.datajpa.repository;
import study.datajpa.entity.Member;
import java.util.List;
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
MemberRepositoryQueryImpl
package study.datajpa.repository;
import lombok.RequiredArgsConstructor;
import study.datajpa.entity.Member;
import javax.persistence.EntityManager;
import java.util.List;
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
MemberRepository
package study.datajpa.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;
import study.datajpa.dto.MemberDto;
import study.datajpa.entity.Member;
import javax.persistence.LockModeType;
import javax.persistence.QueryHint;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
...
주의점
- 항상 사용자 정의 리포지토리가 필요한건 아닙니다. 오히려 복잡해질 수 있기 때문에 서비스를 살펴보고 필요한 경우에 사용합니다.
- 화면에 맞춘 쿼리와 핵심비즈니스 로직을 구분해야합니다. 구분할 때는 그냥 임의의 리포지토리를 만들어서 구분해도 됩니다.
임의의 리포지토리 구분해 사용하기
package study.datajpa.repository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import study.datajpa.entity.Member;
import javax.persistence.EntityManager;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class MemberQueryRepository {
private final EntityManager em;
List<Member> findAllMembers() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
Auditing
엔티티 생성, 변경할 때 변경한 사람과 시간을 추적할 때 사용합니다.
- 등록일
- 수정일
- 등록자
- 수정자
순수 JPA로 해결
사용법
Entity에서 JpaBaseEntity를 extends만 하면 됩니다.
package study.datajpa.entity;
import lombok.Getter;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.time.LocalDateTime;
@MappedSuperclass
@Getter
public class JpaBaseEntity {
@Column(updatable = false)
private LocalDateTime createDate;
private LocalDateTime updateDate;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
createDate = now;
updateDate = now;
}
@PreUpdate
public void preUpdate() {
updateDate = LocalDateTime.now();
}
}
...
public class Member extends JpaBaseEntity{
...
주의점
persist시에도 update에 값을 넣어놓습니다. 왜냐하면 나중에 쿼리할때 null있으면 불편하기 때문입니다.
JpaBaseEntity에 @MappedSuperclass를 해야합니다. @MappedSuperclass의 의미는 진짜 상속관계는 아니고 데이터만 공유하는 상속관계라는 의미입니다.
스프링 데이터 JPA 사용 (실무)
- 순수 JPA 사용때와 달리 어노테이션을 이용하면 됩니다.
- 추가로 @EntityListeners(AuditingEntityListener.class)를 꼭 해주어야 합니다.
- Application의 @SpringBootApplication와 같이 @EnableJpaAuditing을 해줍니다.
실무에서는 어느곳에선 등록일, 수정일만 필요, 등록자 수정자가 필요하지 않는 경우가 있을 수 있습니다.
이럴경우 BaseEntity에 시간, 사람을 다 넣으면 필요없는 데이터가 DB에 들어갑니다.
시간만 필요한 (거의 모든경우)에는 BaseTimeEntity를 최상위에 만들고 그 바로 아래 등록자 수정자를 담은 BaseEntity를 만들면 됩니다.
- 시간, 사람 필요한 경우에는 BaseEntity를 상속
- 시간 필요한 경우에는 BaseTimeEntity를 상속
또한, String을 넣어줄 때는 시간과 달리 string을 넣어줘야 하므로 Application main 함수 아래에 AuditorAware<String>으로 아무 아이디를 넘기도록 합니다.
실무에서는 세션에서 ID를 받아 넘겨주면 됩니다.
package study.datajpa.entity;
import lombok.Getter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity extends BaseTimeEntity{
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
package study.datajpa.entity;
import lombok.Getter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
Web 확장 - 페이징과 정렬
스프링 MVC에서 스프링 데이터가 제공하는 페이징과 정렬 기능을 사용할 수 있습니다.
이 전에 사용했던 Pageable을 사용하면 쉽게 사용이 가능합니다.
pageable의 page, size를 스프링 부트가 pageRequest를 생성해서 injection 해줍니다.
size의 기본값은 20으로 되어있습니다.
Pageable의 page, size 사용자 지정하기
1. 기본 값 바꾸기
글로벌 설정으로 모든 pageable의 size를 설정할 수 있습니다.
application.yml
data:
web:
pageable:
default-page-size: 10
max-page-size: 2000
2. 해당 요청에 대해서만 바꾸기
@PageableDefault로 size를 지정할 수 있습니다. (아래 코드 예시)
@GetMapping("/members")
public Page<Member> list(@PageableDefault(size = 5, sort = "username") Pageable pageable) {
return memberRepository.findAll(pageable);
}
그리고 실제 위 @GetMapping에 Postman으로 요청을 보내고 Json 응답을 받아보았습니다.
지정한 size, sort 대로 결과가 잘 나왔습니다.
근데 마지막 부분에 "pageable" 데이터 부분과 그 아래에 다른 데이터가 반환되는 것을 볼 수 있습니다.
반환타입이 Page라서 해당하는 정보들이 나가는 것을 볼 수 있습니다.
{
"content": [
{
"id": 1,
"username": "user0",
"age": 0,
"team": null
},
{
"id": 2,
"username": "user1",
"age": 1,
"team": null
},
{
"id": 11,
"username": "user10",
"age": 10,
"team": null
},
{
"id": 12,
"username": "user11",
"age": 11,
"team": null
},
{
"id": 13,
"username": "user12",
"age": 12,
"team": null
}
],
"pageable": {
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"offset": 0,
"pageSize": 5,
"pageNumber": 0,
"paged": true,
"unpaged": false
},
"last": false,
"totalPages": 20,
"totalElements": 100,
"number": 0,
"size": 5,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"first": true,
"numberOfElements": 5,
"empty": false
}
Page의 totalCount 계산을 위해 count 쿼리가 나가는 것도 볼 수 있습니다.
추가로 페이징 정보가 둘 이상일때 접두사로 구분이 가능합니다.
@Qualifier에 접두사명을 추가합니다. "{접두사명}_xxx"
ex) /members?member_page=0%order_page=1
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
주의할 점
여기서 주의할 점이 있습니다.
컨트롤러에서는 클라이언트에 절대 엔티티를 그대로 반환하면 안됩니다.
Page도 마찬가지 입니다.
2021.04.05 - [Web/JPA] - EP1. 회원관리 API 만들기
엔티티를 직접 반환하지 않고 DTO로 변환한 뒤 반환합니다.
@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5) Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
// Page<MemberDto> map = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));
Page<MemberDto> map = page.map(MemberDto::new);
return map;
}
참고
Page<MemberDto> map = page.map(MemberDto::new); 사용방법
DTO는 엔티티를 봐도 되지만 엔티티는 DTO를 보면 안되는 원리를 이용해서 아래와 같이 DTO 생성자를 만들 수 있습니다.
package study.datajpa.dto;
import lombok.Data;
import study.datajpa.entity.Member;
@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 member) {
this.id = member.getId();
this.username = member.getUsername();
}
}
page를 1부터 시작하기
현재는 page가 0부터 시작합니다.
page가 1부터 시작하도록 하는 방법이 2가지가 있습니다.
1. 직접 PageRequest 만들기
2. one-indexed-parameters : true 하기
data:
web:
pageable:
default-page-size: 10
max-page-size: 2000
one-indexed-parameters: true
위 방식의 한계는 반환되는 json의 page 정보는 원래와 똑같이 반환됩니다.
추천하는 방식은 그냥 Page를 0부터 시작하는 것 입니다.
'Web > JPA' 카테고리의 다른 글
[Data JPA] EP 6. 나머지 기능들 (0) | 2021.06.01 |
---|---|
[Data JPA] EP 5. 스프링 데이터 JPA 분석 (1) | 2021.06.01 |
[Data JPA] EP 3. 쿼리 메소드 기능 -2 (0) | 2021.05.28 |
[Data JPA] EP 2. 쿼리 메소드 기능 (0) | 2021.05.26 |
[Data JPA] EP 1. 공통 인터페이스 기능 (0) | 2021.05.25 |