스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성한다.
메서드를 직접 구현하고 싶다면 어떻게 해야될까?
인터페이스를 상속받아서 직접 구현하려면 구현해야 하는 기능이 너무 많다.
이때 다음과 같이 사용자 정의 리포지토리를 구현할 수 있다.
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 {
...
}
memberRepository.findMemberCustom();
Impl 로 명명해야 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록한다.@EnableJpaRepositories에 respotirogyImplementationPostfix 속성을 이용하면 된다.Impl 방식도 지원한다. (이 방법을 더 권장)엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶다면?
@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();
}
}
@EntityListeners(AuditingEntityListener.class)
@Getter
@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;
}
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(UUID.randomUUID().toString());
}
@EntityListeners(AuditingEntityListener.class) 적용@EnableJpaAuditing 적용AuditorAware를 스프링 빈으로 등록하면 된다.
@EnableJpaAuditing(modifyOnCreate = false)옵션을 사용한다. (권장x)@EntityListeners(AuditingEntityListener.class)를 생략하고 글로벌로 적용하려면 orm.xml에 등록하면 된다.등록시간, 수정시간만 필요한 경우가 있을 수 도 있기 때문에 다음과 같이 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;
}
HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩 해주는 기능
@GetMapping("members/{id}")
public String findMember(@PathVariable("id") Long id) {
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
@GetMapping("members/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
주의
도메인 클래스 컨버터로 받은 엔티티는 단순 조회용으로만 사용해야 한다. (detach 상태임)
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있게 해주는 기능
@GetMapping("members")
public Page<Member> list(Pageable pageable) {
return memberRepository.findAll(pageable);
}
spring.data.web.pageable.default-page-size=(기본페이지 사이즈 기본값: 20)
spring.data.web.pageable.max-page-size=(최대 페이지 사이즈 기본값: 2000)
@PageableDefault
@GetMapping("members")
public String list(@PageableDefault(size = 12, sort = “username”, direction = Sort.Direction.DESC) Pageable pageable) {
...
}
@Qualifier에 접두사명 추가 “{접두사명}_xxx”
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
스프링 데이터 JPA 공통 인터페이스를 상속한 인터페이스가 동작하는 이유는
애플리케이션 실행 시점에 스프링 데이터 JPA가 구현체를 직접 생성해주기 때문이다.
그렇다면 실제 구현체는 무엇이고 어떻게 구현되어 있을까?
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> ...{
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
...
}
org.springframework.data.jpa.repository.support.SimpleJpaRepository에 존재@Repository 적용 : JPA 예외를 스프링이 추상화한 예외로 변환@Transactional이 이미 적용되어 있음
readOnly = true 적용 : 내부적으로 성능 최적화를 하고 있음save() 메서드 : 새로운 엔티티면 저장 아니면 병합스프링 데이터 JPA 공통 인터페이스의 save(S) 메서드는 새로운 엔티티면 저장 아니면 병합을 수행한다.
그렇다면 새로운 엔티티를 판단하는 기준은 무엇일까?
null로 판단0으로 판단Persistable 인터페이스를 구현해서 판단 로직 변경 가능참고
JPA 식별자 생성 전략이@GenerateValue면 save 호출 시점에 식별자가 없으므로 새로운 엔티티로 인식해서 정상 동작한다.
하지만 식별자를 직접 할당하면 이미 식별자 값이 있는 상태로 save를 호출하기 때문에 merge가 호출된다.(비효율적)
따라서Persistable를 사용해서 새로운 엔티티 확인 여부를 직접 구현하는게 효과적이다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
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;
}
}
getId()와 isNew()를 상속받아서 구현한다.DTO 직접 조회를 좀 더 편하게 사용할 수 있게 해주는 기능
public interface UsernameOnly {
String getUsername();
}
public interface MemberRepository ... {
List<UsernameOnly> findProjectionsByUsername(String username);
}
select m.username from member m where m.username = ?
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age + }"
String getUsername();
}
인터페이스가 아닌 구체적인 DTO 형식도 가능
public class UsernameOnlyDto {
private final String username;
public UsernameOnlyDto(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
Generic type을 줘서 동적으로 프로젝션 데이터 변경 가능
<T> List<T> findProjectionsByUsername(String username, Class<T> type);
public interface NestedClosedProjection {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
}
}
select
m.username as col_0_0_,
t.teamid as col_1_0_,
t.teamid as teamid1_2_,
t.name as name2_2_
from
member m
left outer join
team t
on m.teamid=t.teamid
where
m.username=?
가급적 사용하지 않는게 좋다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "select * from member where username = ?", nativeQuery = true)
Member findByNativeQuery(String username);
}
여러 방법이 있지만 복잡하고 최근에 지원하기 시작한 Projections를 사용하는게 좋다.
public interface MemberProjection {
Long getId();
String getUsername();
String getTeamName();
}
@Query(value = "SELECT m.member_id as id, m.username, t.name as teamName FROM member m left join team t",
countQuery = "SELECT count(*) from member",
nativeQuery = true)
Page<MemberProjection> findByNativeProjection(Pageable pageable);
네이티브 SQL을 DTO로 조회할 때는 JdbcTemplate or myBatis 권장