사용자 정의 리포지토리 구현
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
@RequiredArgsConstructor
public class MemberRepositoryCustomImpl 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 {
...
}
사용자 정의 구현 클래스 규칙
리포지토리 인터페이스 이름 + Impl
사용자 정의 인터페이스 명 + Impl 방식도 지원한다
- 이 방식이 사용자 정의 인터페이스 이름과 구현 클래스 이름이 비슷하므로 더 직관적이다
스프링 데이터 JPA가 인식해서 스프링 빈으로 등록한다
Impl 대신 다른 이름으로 변경하고 싶으면 XML이나 JavaConfig를 설정하여 바꿀 수 있으나 협업과 유지보수 관점에서 관례를 따르는 것을 권장한다
실무에서는 주로 QueryDSL이나 SpringJdbcTemplate을 Spring Data JPA 리포지토리와 함께 사용할 때 사용자 정의 리포지토리 기능 자주 사용한다
주의사항
항상 사용자 정의 리포지토리가 필요한 것은 아니다
그냥 임의의 리포지토리를 만들어도 된다
예를들어 MemberQueryRepository를 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록해서 그냥 직접 사용해도 된다
물론 이 경우 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다
Auditing
엔티티를 생성, 변경할 때 변경한 시간과 사람을 추적하는 방법이다
순수 JPA 사용
@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();
}
}
@Entity
...
public class Member extends JpaBaseEntity {
...
}
@SpringBootTest
@Transactional
@Rollback(value = false)
class MemberTest {
@PersistenceContext
EntityManager em;
@Autowired
MemberRepository memberRepository;
...
@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.getCreatedDate() = " + findMember.getCreatedDate());
// findMember.getCreatedDate() = 2023-02-24T09:24:11.249286
System.out.println("findMember.getUpdatedDate() = " + findMember.getUpdatedDate());
// findMember.getUpdatedDate() = 2023-02-24T09:24:11.398898
}
}
@MappedSuperclass
를 사용하여 엔티티를 생성하지 않고 속성만 엔티티에 상속한다JPA의 주요 이벤트 어노테이션을 활용한다
@PrePersist
@PostPersist
@PreUpdate
@PostUpdate
스프링 데이터 JPA 사용
@MappedSuperclass
@Getter
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createBy;
@LastModifiedBy
private String lastModifiedBy;
}
@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());
// 예제라서 UUID로 AuditorAware를 생성했다
// 실무에서는 스프링 시큐리티 컨텍스트 접근해서 세션 정보로 접근해 아이디로 생성한다
}
}
@Entity
...
public class Member extends BaseEntity {
...
}
@SpringBootTest
@Transactional
@Rollback(value = false)
class MemberTest {
@PersistenceContext
EntityManager em;
@Autowired
MemberRepository memberRepository;
...
@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.getCreatedDate() = " + findMember.getCreatedDate());
// findMember.getCreatedDate() = 2023-02-24T10:14:56.002004
System.out.println("findMember.getUpdatedDate() = " + findMember.getLastModifiedDate());
// findMember.getUpdatedDate() = 2023-02-24T10:14:56.134647
System.out.println("findMember.getCreateBy() = " + findMember.getCreateBy());
// findMember.getCreateBy() = 7ec796ff-74db-4dbd-9061-e953cc5ecd12
System.out.println("findMember.getLastModifiedBy() = " + findMember.getLastModifiedBy());
// findMember.getLastModifiedBy() = 4a014eaa-3c88-4c4d-ba69-23d6ffdc368e
}
}
실무에서 대부분의 엔티티는 등록시간, 수정시간이 필요하지만, 등록자, 수정자는 없을 수도 있다
- BaseTimeEntity와 BaseEntity로 분리하고, 원하는 타입을 선택해서 상속한다
BaseEntity에서
@EntityListeners(AuditingEntityListener.class)
,@CreatedBy
,@LastModifiedBy
를 사용해 등록자, 수정자를 처리해주기 위해서는 어플리케이션 클래스에@EnableJpaAuditing
를 추가하고 AuditorAware 스프링 빈을 등록해야 한다@EntityListeners(AuditingEntityListener.class)
를 생략하고 스프링 데이터 JPA 가 제공하는 이벤트를 엔티티 전체에 적용하려면 META-INF/orm.xml를 만들어 등록하면 된다
저장시점에 등록일, 등록자는 물론이고, 수정일, 수정자도 같은 데이터를 저장한다
데이터가 중복 저장되는 것 같지만, 이렇게 해두면 변경 컬럼만 확인해도 마지막에 업데이트한 유저를 확인 할 수 있으므로 유지보수 관점에서 편리하다
이렇게 하지 않으면 변경 컬럼이 null 일때 등록 컬럼을 또 찾아야 한다
만약 저장시점에 저장데이터만 입력하고 싶으면
@EnableJpaAuditing(modifyOnCreate = false)
옵션을 사용하면 된다
Web확장 - 도메인 클래스 컨버터
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
@PostConstruct
public void init() {
memberRepository.save(new Member("userA"));
}
}
HTTP 요청은 회원 id 를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환받을 수 있다
- 도메인 클래스 컨버터도 리파지토리를 사용해서 엔티티를 찾는다
주의사항
트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다
도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다
Web 확장 - 페이징과 정렬
spring:
...
data:
web:
pageable:
default-page-size: 10
max-page-size: 2000
# one-indexed-parameters: true
...
- 글로벌 설정을 지정할 수 있다
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5) Pageable pageable) {
return memberRepository.findAll(pageable)
.map(MemberDto::new);
}
@PostConstruct
public void init() {
for (int i = 0; i < 100; ++i) {
memberRepository.save(new Member("user" + i, i));
}
}
}
파라미터로 Pageable 인터페이스를 받을 수 있다
실제는
org.springframework.data.domain.PageRequest
객체를 받는다@PageableDefault
어노테이션을 사용해 개별 설정할 수 있다페이징 정보가 둘 이상이면
@Qualifier
에 접두사명을 추가해 접두사로 구분한다public String list( @Qualifier("member") Pageable memberPageable, @Qualifier("order") Pageable orderPageable, ...
요청 파라미터
예) /members?page=0&size=3&sort=id,desc&sort=username,desc
page: 현재 페이지, 0부터 시작한다
size: 한 페이지에 노출할 데이터 건수
sort: 정렬 조건을 정의한다
Page를 1부터 시작하기
Pageable, Page를 파리미터와 응답 값으로 사용히지 않고, 직접 클래스를 만들어서 처리한다
그리고 직접 PageRequest(Pageable 구현체)를 생성해서 리포지토리에 넘긴다
물론 응답값도 Page 대신에 직접 만들어서 제공해야 한다
spring.data.web.pasgeable.one-indexed-parameters
를 true 로 설정한다
그런데 이 방법은 web에서 page 파라미터를 -1 처리 할 뿐이다
따라서 응답값인 Page 에 모두 0 페이지 인덱스를 사용하는 한계가 있다
Page를 0부터 사용하는 것을 권장한다
참고 자료