Spring Data JPA - 2편
사용자 정의 리포지토리 구현
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();
}
}
위처럼 Custom 인터페이스를 만들어 두고, 기존의 Repository 에 extends 하면 된다.
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
...
}
사용자 정의 리포지토리를 만들어두지 않으면 impl 을 만들 때 JpaRepository 의 모든 기능을 구현해야하는 문제가 있다.
보통 복잡한 동적쿼리로 QueryDSL 을 쓸 때 사용자 정의 리포지토리를 많이 사용한다.
참고로 항상 사용자 정의 리포지토리가 필요한 것은 아니다.
그냥 임의의 리포지토리를 만들어도 된다. 예를 들어 MemberQueryRepository 를 인터페이스가 아닌 클래스로 만들고
스프링 빈으로 등록해서 그냥 직접 사용해도 된다. 물론 이 경우에는 스프링 데이터 JPA 와는 아무런 관계 없이 별도로 동작한다.
주의할점
사용자 정의 리파지토리의 구현체는 리파지토리 명 뒤에 Impl
을 붙여야한다.
그래야 Spring Data JPA 가 impl 을 찾아서 넣어준다. (안붙이면 에러남)
Impl 대신 다른 이름을 사용하는 방법이 있기는 하지만.. 그냥 왠만해서는 관례상 Impl 작성을 따르자.
Auditing
엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적할 때 사용.
- 등록일
- 수정일
- 등록자
- 수정자
순수 JPA 방식
@Getter
@MappedSuperclass
public class JpaBaseEntity {
@Column(updatable = false)
private LocalDateTime createdDate;
private LocalDateTime updatedDate;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
createdDate = now;
updatedDate = now;
}
@PreUpdate
public void preUpdate() {
updatedDate = LocalDateTime.now();
}
}
@Entity
public class Member extends JpaBaseEntity {
...
}
위처럼 등록해두면 등록일, 수정일 처리를 공통 클래스로 처리할 수 있다.
Spring Data JPA 에서는 이것을 더 편하게 만들 수 있도록 제공한다.
Spring Data JPA 방식
먼저 이걸 사용하려면 스프링 부트 설정 클래스에 @EnableJpaAuditing 어노테이션을 적용해야한다. (이거 안하면 적용안되니 주의)
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
}
그 후 아래와 같이 등록하면 된다.
@Getter
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
@CreatedBy
, @LastModifiedBy
는 어떤 이름을 넣어줄지 처리를 해주어야한다.
@Bean
public AuditorAware<String> aditorProvider() {
// 스프링 시큐리티를 쓰면 해당 세션 정보 ID 를 가져와서 넣어주면 됨
}
@EntityListeners(AuditingEntityListener.class)
자동으로 넣기 위해서는 해당 어노테이션을 붙여줘야하는데,, 매번 넣어주기 번거롭다면 xml 로 처리
할 수 있다.
경로 : META-INF/orm.xml
<?xml version=“1.0” encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/ orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd"
version="2.2">
<persistence-unit-metadata> <persistence-unit-defaults>
<entity-listeners> <entity-listener
class="org.springframework.data.jpa.domain.support.AuditingEntityListener”/> </entity-listeners>
</persistence-unit-defaults> </persistence-unit-metadata>
</entity-mappings>
JPA 스펙에서 entity-listener 를 읽어서 자동으로 실행함.
등록 시 update 시간에 null 을 넣어야 할까?
update 에 null 을 넣게되면 null 처리해야 할 코드가 늘어난다.
보통 관례 상 insert 시 update 도 등록 시간을 넣어주는것이 일반적이다.
실무에서는 어떻게 활용할까
보통 실무에서는 등록시간, 수정시간
만 사용하는 페이지가 있고등록시간, 수정시간, 등록자, 수정자
를 사용하는 페이지가 있을 수 있다. (보통 시간은 거의 다 쓴다.)
이런 경우 아래와 같이 처리 할 수 있다.
@Getter
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
@Getter
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
등록시간, 수정시간
필요 -> BaseTimeEntity 사용등록시간, 수정시간, 등록자, 수정자
필요 -> BaseEntity 사용
도메인 클래스 컨버터
보통 유저 정보를 조회할 때 아래와 같이 @PathVariable 을 통해 id 를 가져와서 컨트롤러를 만든다.
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id) {
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
하지만 클래스로 받아서 처리할 수 도 있다.
@GetMapping("/members2/{id}")
public String findMember2(@PathVariable("id") Member member) {
return member.getUsername();
}
HTTP 요청은 회원 id
를 받지만 스프링의 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 조회해 반환한다.
이 때, 도메인 클래스 컨버터도 리파지토리를 사용해 엔티티를 조회한다.
주의 : 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 해당 엔티티는 단순 조회용으로만 사용해야 한다.
(트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.)
영한님은 잘 사용하지는 않는다고 하심.
컨트롤러에서 페이징 처리
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
return memberRepository.findAll(pageable);
}
스프링 부트가 자동으로 PageRequest
객체를 생성해서 Pageable 인터페이스에 DI 해준다.
따라서 클라이언트에서는 아래와 같이 사용하면 된다.
http://localhost:8080/members?page=0&size=3&sort=username,desc
- page : 현재 페이지, 0 부터 시작
- size : 한 페이지에 노출할 데이터 건수
- sort : 정렬 조건
page default size 변경
기본적으로 페이징 데이터는 20개씩 가져오도록 세팅되어있다.
이것을 변경하려면 아래와 같이 지정하면 된다.
글로벌 설정
application.yml
spring:
data:
web:
pageable:
default-page-size: 10
단건 설정
@PageableDefault
@GetMapping("/members")
public Page<Member> list(@PageableDefault(size = 5, sort = "username") Pageable pageable) {
return memberRepository.findAll(pageable);
}
API 에서는 반환을 Entity 로 하면 안된다.
API 스팩이 변하기 때문에 DTO 로 변환해서 반환해야한다.
@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5, sort = "username") Pageable pageable) {
return memberRepository.findAll(pageable)
.map(member -> new MemberDto(member.getId(), member.getUsername(), null));
}
DTO 와 Entity 의존 관계
DTO 는 Entity 의 의존을 가져도 된다.
하지만 Entity 는 가급적 DTO 의 의존을 가지면 안된다.
@Data
public class MemberDto {
private Long id;
private String username;
private String teamName;
public MemberDto(Member member) {
this.id = member.getId();
this.username = member.getUsername();
}
}
@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5, sort = "username") Pageable pageable) {
return memberRepository.findAll(pageable)
.map(MemberDto::new);
}
Page 를 1부터 시작하려면?
스프링 데이터는 Page 를 0 부터 시작한다.
1부터 시작하게 하려면 아래와 같이 처리하면 된다.
방법 1
Pageable, Page 를 파라미터와 응답 값으로 사용하지 않고, 직접 클래스를 만들어서 처리한다.
그리고 직접 PageRequest 를 생성해서 리포지토리에 넘긴다.
물론 응답값도 Page 대신에 직접 만들어서 제공해야 한다.
방법 2
spring.data.web.pageable.one-indexed-parameters
를 true
로 설정한다.
하지만 이 방법은 web 에서 page 파라미터를 -1 처리 할 뿐이다.
(page=0, page=1 입력 모두 동일한 데이터를 노출함 - 모두 0 페이지 인덱스를 사용)
이것의 단점은 같이 넘어가는 Page 데이터는 현재 페이지 정보를 보여주기 때문에 데이터가 일치하지 않는 문제가 있다.
그냥 Page 를 0 부터 시작하는 것으로 쓰는게 가장 깔끔하다.
참고
- 실전! 스프링 데이터 JPA
'JPA' 카테고리의 다른 글
Querydsl - 1편 (0) | 2022.02.08 |
---|---|
JPA 를 공부하면서 알게 된 내용 정리 4 (0) | 2022.02.05 |
JPA 를 공부하면서 알게 된 내용 정리 2 (0) | 2022.01.29 |
JPA 를 공부하면서 알게 된 내용 정리 1 (0) | 2022.01.26 |
다양한 연관관계 매핑 (0) | 2020.04.09 |
댓글