인프런 김영한 강사님의
실전! 스프링 데이터 JPA
을 정리한 글입니다.
스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고, 구현체는 스프링이 자동 생성
스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많다.
만약 인터페이스의 메서드를 직접 구현하고 싶다면?
EntityManager
)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")
.getResultList();
}
}
public interface MemberRepository
extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
List<Member> result = memberRepository.findMemberCustom();
규칙 : 리포지토리 인터페이스 이름 + Impl
스프링 데이터 JPA가 인식해서 스프링 빈으로 등록
Impl
방식도 지원<repositories base-package="study.datajpa.repository"
repository-impl-postfix="Impl" />
@EnableJpaRepositories(basePackages = "study.datajpa.repository",
repositoryImplementationPostfix = "Impl")
참고 : 실무에서는 주로 QueryDSL이나 SpringJdbcTemplate을 함께 사용할 때 사용자 정의 리포지토리 기능을 자주 사용 한다.
참고 : 항상 사용자 정의 리포지토리가 필요한 것은 아니고, 그냥 임의의 리포지토리를 만들어도 상관 없다.
impl
@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
package study.datajpa.entity;
@MappedSuperclass
@Getter
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();
}
}
public class Member extends JpaBaseEntity {}
@Test
public void jpaEventBaseEntity() throws Exception {
//given
Member member = new Member("member1");
memberRepository.save(member); //@PrePersist
Thread.sleep(100);
member.setUsername("member2");
em.flush(); //@PreUpdate
em.clear();
//when
Member findMember = memberRepository.findById(member.getId()).get();
//then
System.out.println("findMember.createdDate = " +
findMember.getCreatedDate());
System.out.println("findMember.updatedDate = " +
findMember.getUpdatedDate());
}
@PrePersist, @PostPersist
@PreUpdate, @PostUpdate
@EnableJpaAuditing
: 스프링 부트 설정 클래스에 적용해야 함@EntityListeners(AuditingEntityListener.class)
: 엔티티에 적용package study.datajpa.entity;
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
package jpabook.jpashop.domain;
@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;
}
AuditorAware
스프링 빈 등록@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(UUID.randomUUID().toString());
}
}
주의 : DataJpaApplication
에 @EnableJpaAuditing
도 함께 등록해야 함.
실무에서는 세션 정보나 스프링 시큐리티 로그인 정보에서 ID를 얻는다.
참고 : 실무에서 대부분의 엔티티는 등록시간, 수정시간이 필요하지만, 등록자, 수정자는 없을 수도 있다.
따라서 아래와 같이 Base 타입을 분리하고, 원하는 타입을 선택해서 상속한다.
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
참고 : 저장 시점에 등록일, 등록자, 수정일, 수정자가 같은 데이터가 저장된다.
중복저장 같지만, 이렇게 해두면 변경 컬럼만 확인해도 마지막에 업데이트한 유저를 확인할 수 있으므로, 유지보수 관점에서 편하다.
이렇게 하지 않으면 변경 컬럼이null
일때 등록 컬럼을 또 찾아야 한다.
참고 : 저장시점에 저장데이터만 입력하고 싶으면
@EnableJpaAuditing(modifyOnCreate = false)
옵션 사용
@EntityListeners(AuditingEntityListener.class)
를 생략하고 스프링 데이터 JPA가 제공하는 이벤트를 엔티티 전체에 적용하려면 orm.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>
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id) {
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
}
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
}
HTTP 요청은 회원 id
를 받지만, 도메인 클래스 컨버터가 중간에동작해서 회원 엔티티를 반환
도메인 클래스 컨버터로 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
파라미터로 Pageable
을 받을 수 있다.
Pageable
은 인터페이스, 실제는 org.springframework.data.domain.PageRequest
객체 생성
ex> /members?page=0&size=3&sort=id,desc&sort=username,desc
page : 현재 페이지, 0부터 시작
size : 한 페이지에 노출할 데이터 건수
sort : 정렬 조건을 정의
sort
파라미터 추가 (asc
생략 가능)spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/
@PageableDefault
어노테이션 사용@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = “username”,
direction = Sort.Direction.DESC) Pageable pageable) {
...
}
페이징 정보가 둘 이상이면 접두사로 구분
@Qualifier
에 접두사명 추가 ("{접두사명}_xxx")
ex> /members?member_page=0&order_page=1
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
map()
을 지원해서 내부 데이터를 다른 것으로 변경할 수 있다.@Data
public class MemberDto {
private Long id;
private String username;
public MemberDto(Member m) {
this.id = m.getId();
this.username = m.getUsername();
}
}
Page.map()
사용@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
Page<MemberDto> pageDto = page.map(MemberDto::new);
return pageDto;
}
Page.map()
코드 최적화@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
return memberRepository.findAll(pageable).map(MemberDto::new);
}
스프링 데이터는 Page를 0부터 시작한다.
만약 1부터 시작하려면?
Pageable, Page를 파라미터와 응답 값으로 사용하지 않고, 직접 클래스를 만들어서 처리한다.
spring.data.web.pageable.one-indexed-parameters
를 true
로 설정한다.
page
파라미터를 -1
처리 할 뿐이다.Page
에 모두 0 페이지 인덱스를 사용하는 한계가 있다.one-indexed-parameters
Page 1 요청 (http://localhost:8080/members?page=1
){
"content": [
...
],
"pageable": {
"offset": 0,
"pageSize": 10,
"pageNumber": 0 //0 인덱스
},
"number": 0, //0 인덱스
"empty": false
}