@Setter: 실무에서 가급적 Setter는 사용하지 않기 -> 연관관계 편의 메소드 사용
@NoArgsConstructor AccessLevel.PROTECTED: 기본 생성자 막고 싶은데, JPA 스팩상 PROTECTED로 열어두어야 함
@ToString은 가급적 내부 필드만(연관관계 없는 필드만)
스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하면 일반적인 레포지토리의 공통 메서드를 사용할 수 있다.
// JpaRepository를 상속받는 인터페이스 생성, @Repository 생략 가능, 제네릭은 <엔티티 타입, 식별자 타입>으로 설정
public interface MemberRepository extends JpaRepository<Member, Long> {
}
엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다.
// JPA에서 findByUsernameAndAgeGreaterThan 이름을 보고 내부적으로 쿼리 생성
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username= :username and m.age = :age")
List<Member> findUser(@Param("username") String username, @Param("age") int
age);
}
실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라고 할 수 있다.
참고소 실무에서는 이름으로 쿼리 생성 기능은 파라미터가 많아지면 이름이 매우 지저분해지므로 @Query 기능을 자주 사용한다.
List<Member> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional
파라미터 : Sort(정렬 가능), Pageable(페이징 기능, 내부에 Sort 포함)
반환 타입 : Page(count 쿼리 결과를 포함하는 페이징), Slice(count 쿼리 없이 다음 페이지만 확인 가능), List(counst 쿼리 없이 결과만 반환)
사용 예시
Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Sort sort);
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC,
"username"));
Page<Member> page = memberRepository.findByAge(10, pageRequest);
List<Member> content = page.getContent(); //조회된 데이터
assertThat(content.size()).isEqualTo(3); //조회된 데이터 수 assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수 assertThat(page.getNumber()).isEqualTo(0); //페이지 번호 assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호 assertThat(page.isFirst()).isTrue(); //첫번째 항목인가? assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
Pageable은 인터페이스이므로 사용할 때는 구현 객체는 PageRequest를 사용한다.
첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다.
여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다. 페이지는 0부터 시작한다.
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
참고 : 스프링 부트 3이상부터 하이버네이트 6이 적용되는데, 의미없는 left join을 최적화한다. 안쓰는 경우에도 조인을 하고 싶으면 fetch join을 사용하면 된다.
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
@Modifying 어노테이션을 사용한다. 영속성 컨텍스트 초기화를 자동으로 하고 싶으면 clearAutomatically = true를 적용하면 된다. 벌크연산은 영속성 컨텍스트를 무시하기 때문에 영속성 컨텍스트에 엔티티가 없는 상태 또는 벌크 연산 실행 후 영속성 컨텍스트 초기화를 시켜주어야 한다.
스프링 데이터 JPA는 페치 조인 기능을 간편하게 지원한다. 엔티티 그래프 기능을 편리하게 사용할 수 있다.
//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"}) List<Member> findAll();
//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"}) @Query("select m from Member m") List<Member> findMemberEntityGraph();
//메서드 이름으로 쿼리에서 특히 편리하다.
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)
JPA 구현체에게 제공하는 힌트이다.
사용 예시
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly",
value = "true")},
forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);
락 기능도 어노테이션으로 제공된다
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
커스텀 인터페이스를 만들어 메소드를 정의하고 구현 클래스를 만들어 구현한다. JpaRepository를 상속받은 인터페이스가 커스텀 인터페이스를 상속받으면 된다.
// 커스텀 인터페이스
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
// 커스텀 구현 클래스에서 커스텀 인터페이스를 상속받고 실제 코드 구현
public class MemberRepositoryImpl implements MemberRepositoryCustom {
public List<Member> findMemberCustom() {
} }
// Data Jpa를 상속받은 리포지토리에 커스텀 인터페이스 상속
public interface MemberRepository
extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
참고 : 커스텀 인터페이스를 구현한 클래스는 사용자 정의 인터페이스 명 + "Impl"로 작명해야한다.
엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하는 기능을 제공한다.
@MappedSuperClass를 사용하는데, 클래스를 상속하지 않고 테이블만 상속할 때 사용한다.
스프링 데이터 JPA를 사용하지 않으면 @PrePersist, @PostPersist, @PreUpdate, @PostUpdate를 사용한다.
스프링 데이터 JPA가 제공하는 어노테이션을 사용하려면
@EnableJpaAuditing을 스프링 부트 설정 클래스에 적용하고 @EntityListeners(AuditingEntityListener.class)를 엔티티에 적용한다.
그러면 @CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy를 엔티티에 적용할 수 있다.
예시)
// 실무에서는 등록자, 수정자가 없을 수도 있으므로 시간과 Base를 분리하고 원하는 타입을 선택해서 상속한다.
@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` 스프링 빈 등록. 실무에서는 세션 정보나 시큐리티 로그인 정보에서 ID를 받는다.
@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());
}
}
HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아 바인딩할 수 있다.
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
}
중간에 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환한다. 리포지토리를 사용해서 엔티티를 찾는다. 이렇게 찾을 경우 단순 조회용으로만 사용해야 한다.
파라미터로 Pageable를 받을 수 있다.
Pageable로 Page 객체를 생성할 수 있다.
요청 파라미터 예시로는 "/members?page=0&size=3&sort=id"가 가능하다.
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
// 설정파일에 글로벌 설정
spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/
// 메서드 개별 설정
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "username",
direction = Sort.Direction.DESC) Pageable pageable) {
...
}
Page.map()을 사용하면 된다.
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
return memberRepository.findAll(pageable).map(MemberDto::new);
}
스프링 데이터의 Page는 0부터 시작한다.
1부터 시작하는 방법
Pageable,Page를 파라미터와 응답 값으로 사용하지 않고 직접 클래스를 만들어 Pageable 구현체를 생성해서 리포지토에 넘긴다. 그 후 응답 값도 Page 대신에 직접 만들어야 한다.
spring.data.web.pageable.one-indexed-parameters 를 true 로 설정하는 방법도 있다.
이 방법은 page 파라미터를 -1 처리하기 때문에 응답값인 Page에 모두 0페이지 인덱스를 사용하는 한계가 있다.
객체일 경우 null 이면 새로운 객체 아니면 기존 객체
자바 기본 타입일 경우 0이면 새로운 객체 아니면 기존 객체
식별자 생성 전략이 자동생성이면 save() 호출 시점에 식별자가 없으므로 새로운 엔티티로 간주하고 정상 동작한다. 하지만 직접 할당 전략이라면 이미 식별자 값이 있으므로 save() 호출시 merge()가 호출된다. merge()는 DB를 호출해서 값이 없으면 새로운 엔티티로 인지하므로 매우 비효율 적이다.
따라서 Persistable을 사용해서 새로운 엔티티 확인 여부를 직접 구현하게 하는 방법이 효과적이다.
참고로 등록시간(@CreatedDate)를 조합해서 사용하면 새로운 엔티티인지 여부를 편리하게 확인할 수 있다.
예시)
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {
@Id
private String id;
@CreatedDate
private LocalDateTime createdDate;
public Item(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public boolean isNew() {
return createdDate == null;
}
}
출처 : 실전! 스프링 데이터 JPA