참고: 최근 IntelliJ 버전은 Gradle로 실행을 하는 것이 기본 설정이다.
이렇게 하면 실행속도가 느리다. 다음과 같이 변경하면 자바로 바로 실행하므로 좀 더 빨라진다.
- Preferences
- Build, Execution, Deployment
- Build Tools
- Gradle
- Build and run using: Gradle -> IntelliJ IDEA
- Run tests using: Gradle -> IntelliJ IDEA
롬복 적용
- Preferences - plugin - lombok 검색 실행 (재시작)
- Preferences - Annotation Processors 검색 - Enable annotation processing 체크 (재시작)
- 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인
공통 인페이스 설정
- 스프링 부트의 경우 어플리케이션 위에 아래의 annotation을 추가하지 않아도 된다.(componentscan을 하기 때문)
- @EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
- public interface TestReopository extends JapRepositroty<Test, Long>
- 인터페이스이고 구현체인데 동작하는 이유 :
- spring-data-jpa가 해당 인터페이스들을 확인하고 프록시로 구현체를 빈에 등록하기 때문
- @Repository가 없어도 빈으로 등록가능
공통 인터페이스 분석
쿼리 메소드 기능
- 메소드 이름으로 쿼리 생성
- NamedQuery
- @Query - 리파지토리 메소드에 쿼리 정의
- 파라미터 바인딩
- 반환 타입
- 페이징과 정렬
- 벌크성 수정 쿼리
- @EntityGraph
메소드 이름으로 쿼리 생성
- 생성 규칙 : https://docs.spring.io/spring-data/jpa/docs/current/ reference/html/#jpa.query-methods.query-creation
- 조회 : find...By
- 숫자 : count...By
- 존재 : exists...By
- 유일 : find...Distinct...By
- 제한 : findFirst3, findFirst, findTop, findTop3
- https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.limit-query-result
필드명이 변경 됐을때, 어플리케이션 실행시점에서 오류를 찾을 수 있다.
NamedQuery(비추)
클래스에 추가
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username"
)
public class Member {
jparepository
public List<Member> findByUsername(String username){
return em.createQuery("Member.findByUsername", Member.class)
.setParameter("username", username)
.getResultList();
}
repository
@Query(name = "Member.findByUsername")
List<Member> findByUsernmae(@Param("username")String usernmae);
- 네임드 쿼리는 app로딩 시점에 쿼리 에러를 발견할 수 있는 장점은 있다.
- 스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행
- 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.
- 필요하면 전략을 변경할 수 있지만 권장하지 않는다.
- 참고: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/ #repositories.query-methods.query-lookup-strategies
@Query, 리포지토리 메소드에 쿼리 정의
@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);
- 이름 없는 NamedQuery라 할 수 있다.
- NamedQuery처럼 app실행 시점에 에러를 발견할 수 있다.
@Query 값, DTO 조회
@Query를 통해 이전과 같이 entitiy를 조회할 수 있지만,
Dto클래스를 아래와 같이 생성자로 만들어 반환할 수 있다. (패키지 경로까지 설정해줘야 함)
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
파라미터 바인딩
이름기반 과 위치기반이 있지만, 이름기반으로 사용하는것이 가독성이 좋다.
이름기반 - :paramname -> 파라미터 이름중 paramname에 대응된다.
// 컬렉션 기반
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
반환타입
- 컬렉션 -> 없으면 빈 컬렉션
- 클래스 -> 없으면 null, 복수 -> NonUniqueResultException
- Optional
사실 단건 클래스 조회가 없으면 NoResultException이 발생하지만, 예외처리로 null을 반환한다.
페이징 / 정렬
순수 JPA
주어진 나이를 가진 사람을, 정렬하여 페이징 처리할건데,
몇번째 페이지를 보여줄지 정한다.
public List<Member> findByPage(int age, int offset, int limit) {
return em.createQuery("select m from Member m where m.age = :age order by m.username desc")
.setParameter("age", age)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
Spring-Data-Jpa
interface Page extends Slice
Page : 추가 count 쿼리 결과를 포함(전체 페이지수, 전체 데이터 수)
Slice : 추가 count 쿼리 결과를 미포함, 다음 페이지만 확인(내부적으로 limit+1 조회)
Page<Member> findByAge(int age, Pageable pageable);
// repository 설정
memberRepository.save(); // 5회 저장 가정
int age = 10;
// 페이지 0, 사이즈 3, sort설정 // 페이징에 필요한 정보
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Direction.DESC, "usernmae"));
Page<Member> page = memberRepository.findByAge(age, pageRequest);
List<Member> content = page.getContent();
//then
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(); //다음 페이지가 있는가?
Page는 1부터 시작이 아니라 0부터 시작이다.
//count쿼리는 조인시 성능에 지장이 될 수 있기에, 쿼리를 분리하여 사용할 수 있도록 설계
@Query(value = “select m from Member m”,
countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);
Top, First
List<Member> findTop3By(); // 가장 위 3개만
페이징 결과 -> 외부
Page<Member> page = memberRepository.findByAge(10, pageRequest);
// dto로 변환
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
벌크성 수정쿼리
순수 JPA
public int bulkAgePlus(int age) {
return em.createQuery(
"update Member m set m.age = m.age + 1"
+ " where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
}
Data-Jpa
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
- @Modifying이 없다면 Not supported for DML operations 발생
- 벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, DB와 1차 캐시의 값이 다르다.
- 이후에 em을 flush 혹은 clear하거나
- @Modifying(clearAutomatically = true) 옵션을 추가해준다.
@EntityGraph
연관관계 매핑에서(보통 Lazy로딩을 사용하므로) 연관된 객체의 데이터(프록시)를 검색할 경우 마다 쿼리가 1개씩 발생한다. -> 연관된 데이터를 1번의 쿼리로 모두 가져올 수 있도록 패치조인/EntityGraph 사용
패치조인
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
EntityGraph
//공통 메서드 오버라이드
@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);
- 패치조인의 간편버전
- LEFT OUTER JOIN을 사용
NamedEntityGraph(비추)
NamedQuery처럼 Entity위에 지정된 EntityGraph를 사용할 수 있다. (생략)
JPA HINT
(SQL힌트가 아니라 JPA구현체에게 제공하는 힌트)
@QueryHints(
value = @QueryHint(name = "org.hibernate.readOnly", value =
"true")
)
Member findReadOnlyByUsername(String username);
// 읽기전용으로 읽어온 데이터에 대해서 변화(update/delete)가 없을 경우, 메모리 최적화 가능(1차캐시 X)
Lock
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
다른 리소스가 접근하지 못하도록 Lock설정 가능
사용자 정의 레포지토리
사용자 정의 인터페이스 생성
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 {
규칙
- 규칙: 리포지토리 인터페이스 이름 + Impl
- 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록
- 바꿀 순 있지만, 그냥 쓰도록 하자
사용
- JdbcTemplate이나 QueryDsl 사용시 주로 사용
- 사용자 정의 인터페이스가 아닌 별도의 클래스를 빈으로 등록하여, 용도(구조적) 분리용으로 사용하여도 된다.
- 스프링 데이터 2.x 부터는 사용자 정의 구현 클래스에 리포지토리 인터페이스 이름 + Impl 을 적용하는 대신에 사용자 정의 인터페이스 명 + Impl 방식도 지원한다.
- 예를 들어서 위 예제의 MemberRepositoryImpl 대신에 MemberRepositoryCustomImpl 같이 구현해도 된다.
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();
}
}
Data-jpa
Envers 적용과 유사
설정
@EnableJpaAuditing // 옵션적용
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
@Bean // 등록, 수정자를 설정하기 위한 설정, 시큐리티 사용중이면 시큐리티 로그인 정보에서 Id 추출
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(UUID.randomUUID().toString());
}
}
@CreatedDate, @lstModifiedDate, @CreatedBy, @LastModifiedBy
어노테이션으로 쉽게 설정가능
@EntityListeners(AuditingEntityListener.class) // 엔티티에도 옵션적용
@MappedSuperclass
@Getter
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;
}
시간과 등록을 분리 사용하기 위해 두 클래스를 상속으로 분리하여, 원하는 추적 정보만 사용하는 것도 좋은 방법이다.
BaseEntitiy extends BaseTimeEntitiy & BaseTimeEntity
웹 확장
도메인 클래스 컨버터(비추) - 간단한 조회용으로만 사용
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id) {
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
@GetMapping("/members2/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
위 아래 동일하게 동작
HTTP 요청은 회원 id 를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환 도메인 클래스 컨버터도 리파지토리를 사용해서 엔티티를 찾음
해당 엔티티는 단순 조회용으로 1차 캐시에 들어가지 않는다.
페이징과 정렬
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
파라미터로 Pageable 을 받을 수 있다. Pageable 은 인터페이스, 실제는 org.springframework.data.domain.PageRequest 객체 생성
예시 ) /members?page=0&size=3&sort=id,desc&sort=username,desc
page: 현재 페이지 (0부터 시작한다)
size: 한 페이지에 노출할 데이터 건수
sort: 정렬 조건을 정의한다.
예) 정렬 속성,정렬 속성...(ASC | DESC), 정렬 방향을 변경하고 싶으면 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 에 접두사명 추가
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable,
...
페이지 내용을 DTO로 변환
Page.map()활용
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
return memberRepository.findAll(pageable).map(MemberDto::new);
}
페이지를 0이 아닌 1부터 시작할 수 있는 방법들이 두가지 정도 존해하지만,
(재정의 혹은 전체 설정변경) 한계점과 복잡도가 존재하므로 그냥 쓰도록 하자~
Spring-Data-Jpa 분석
SimpleJpaRepository : 구현체
- @Repository 적용: JPA 예외를 스프링이 추상화한 예외로 변환
- @Transactional 트랜잭션 적용
- 그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했음(사실은 트랜잭션이 리포지토리 계층에 걸려있는 것임)
- 최상단의 @Transactional(readOnly = true)
- 단순히 조회만 하고 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있음
- 아닌경우는 @Transactional을 별도로 함수마다 설정
- Save()
- 새로운 엔티티면 persist
- 있던 엔티티면 merge
새로운 엔티티를 판단하는 기본 전략
- 식별자가 객체일 때 null 로 판단
- 식별자가 자바 기본 타입일 때 0 으로 판단
- Persistable 인터페이스를 구현해서 판단 로직 변경 가능
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
@GenerateValue를 사용하고 있다면, persist()를 호출하게 되겠지만.
사용하지 못할 경우(id값을 직접 입력)엔 merge를 호출하게 된다. merge는 insert 이전에 select를 실행하는 구조로서 비효율적이다!
이를 해결하기 위해 Persistable을 구현하도록 한다.
@Entity
public class Item implements Persistable<String> {
@Id
private String id;
public Item(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public boolean isNew() {
// 새로운 엔티티인지 아닌지 구분할 수 있는 로직이 필요하다
// (createDate 유무로 판단하는것도 좋은방법이다.)
}
}
나머지 기능들
명세 Specifications(비추)
Jpa Criteria로 사용 -> 가독성이 떨어지고 복잡하므로 비추 -> QueryDsl 사용
조건들을 미리 구현하여 조립하는 느낌
Query By Example(비추)
찾고 싶은 엔티티 객체를 만든 후,
Example.of()를 통해 만든 Example으로 엔티티들 조회할 수 있다.
//ExampleMatcher 생성, age 프로퍼티는 무시
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("age");
Example<Member> example = Example.of(member, matcher);
// 엔티티 객체 member로 같은 조건을 조회
List<Member> result = memberRepository.findAll(example);
장점
- 엔티티 객체 그대로 사용하여 동적 쿼리 용이
- RDB -> NoSql 으로 코드 변화 없이 변경 가능
- Jpa Repository 인터페이스에 기능이 존재함
단점
- 내부조인만 가능, 외부조인 불가능
- 단순한 쿼리 조건만 사용 가능, 중첩 조건 불가능
- QueryDsl이라는 더 좋은 기술이 있다
Projections
엔티티 대신에 DTO를 편리하게 조회할 때 사용
- Close projection
public interface UsernameOnly {
String getUsername();
// 조회할 엔티티의 필드명을 get형식으로 작성
}
public interface MemberRepository ... {
List<UsernameOnly> findProjectionsByUsername(String username);
// 메서드 이름은 자유지만, 반환 타입을 위 인터페이스로 설정
}
스프링이 인터페이스의 구현체를 생성하여 원하는 필드만 조회할 수 있는 쿼리 생성
- open projection
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age + ' ' + target.team.name}") // 엔티티 필드 전체를 가져와서 SpEL문법에서 처리
String getUsername();
// 조회할 엔티티의 필드명을 get형식으로 작성
}
- class projection
실제 클래스 구현
public class UsernameOnlyDto {
private final String username;
public UsernameOnlyDto(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
- 동적 Projection
repository에 제네릭으로 선언하여 사용가능(중복 줄이기)
<T> List<T> findProjectionsByUsername(String username, Class<T> type);
- 중첩 Projection
public interface NestedClosedProjection {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
} // 루트 엔티티가 아니라면 모든 컬럼을 다 select 해온다.
}
정리
- 프로젝션 대상이 root 엔티티면 최적화 되므로 유용하다.
- 프로젝션 대상이 root 엔티티를 넘어가면 LEFT OUTER JOIN 처리 되므로 JPQL SELECT 최적화가 안된다. (모든 필드를 SELECT해서 엔티티로 조회한 다음에 계산)
- 실무의 복잡한 쿼리를 해결하기에는 한계가 있다. 조금만 복잡해지면 QueryDSL을 사용하자
네이티브 쿼리
- 페이징 지원
- 반환 타입
- Object[]
- Tuple
- DTO(Projection 지원)
- 제약
- Sort 파라미터를 통한 정렬이 동작 안 할 수 있다.
- 런타임 오류로만 오류 발견
- 동적 쿼리 불가능
일단 차라리 JdbcTemplate 을 통한 커스텀 레포지토리를 권장함
Projection을 통한DTO반환도 가능
(select 문을 Projection 필드와 일치시켜 준다.)
@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);
'스프링 > JPA' 카테고리의 다른 글
Querydsl (0) | 2021.08.24 |
---|---|
Envers / spring-data-envers (0) | 2021.08.03 |
JPA 최적화, Hint (0) | 2021.06.04 |
JPA 변경/삭제 @Modifying (0) | 2021.06.04 |
FK가 PK가 아닌 다른 컬럼과 연관관계가 있을 때 (0) | 2021.05.31 |
댓글