public interface MemberRepository extends JpaRepository<Member,Long>
{
}
이렇게 해놓으면 Spring Data JPA가 프록시 객체를 만들어서 필요한 곳에 위 인터페이스 구현체를 집어 넣어준다.
@Repository 생략해도 괜찮다.
기본적으로 Spring Data가 공통적으로 제공되는 기술과 특화된 기술을 제공해주는 파트가 나뉘어져있다.
쿼리 메소드 기능
public interface MemberRepository extends JpaRepository<Member,Long>
{
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
public List<Member> findByUsernameAndAgeGreaterThan(String username, int age)
{
return em.createQuery("select m from Member m where m.username = :username and m.age > :age")
.setParameter("username",username)
.setParameter("age",age)
.getResultList();
}
SpringDataJPA가 인터페이스에 저렇게 적기만 해도 아래와 같이 JPQL을 생성해서 쿼리를 날려준다.
SpringDataJPA 메뉴얼에 쿼리생성에 대한 간단한 문법이 나와잇다.
Spring Data JPA - Reference Documentation
네임드 쿼리
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id","username","age"})
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username =:username"
)
public class Member
{
이런식으로 위에 쿼리를 작성할 수 있다. 호출 할떄는 직접
public List<Member> findByUsername(String username)
{
return em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username",username)
.getResultList();
}
Repository 에서 직접쿼리를 호출해서 새로운 메도스로 감싸서 호출할 수도 있고
public interface MemberRepository extends JpaRepository<Member,Long>
{
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
}
Spring Datajpa가 제공해주는 기능을 이용하여 @Query에 정의한 네임드쿼리 이름을 @Param 을 통해서 jpql 의 parameter 인자 값을 넘겨줄 수 있다.
@Query를 생략하더라도 내부적으로 Member(제네릭인자로 주어진 클래스명).findByUsername (메소드명) 로 된 네임드쿼리를 찾고 없으면 쿼리메소드를 생성해준다.
쓰는 이유중 하나는 네임드쿼리는 application loading 시점에 한번 parsing 을 해보기 때문에 개발자의 글자오류를 바로 잡아준다. 그렇지않으면 사용자가 버튼을 누를 때 에러가 뜨는 대참사가 일어 날 수 도 있다.
public interface MemberRepository extends JpaRepository<Member,Long>
{
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
@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);
}
이런식으로 직접 정의해 줄 수 있다. 또한 메소드 명도 쿼리메소드 처럼 길게 안써도 되고 자신이 마음대로 써줄수 있기에 용이하다. 마찬가지로 로딩 시점에 String에 개발자가 만든 오류를 감지해준다.
사실상 이름이 없는 쿼리라고 생각하면 된다.
@Query, 값, DTO 조회하기
public interface MemberRepository extends JpaRepository<Member,Long>
{
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
@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);
@Query("select m.username from Member m")
List<String> findUsernameList();
@Query("select new SpringDataJPA.SpringDataJPA.dto.MemberDto(m.id, m.username,t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
}
MemberDto를 따로 정해주고 jpql을안에서 똑같이 작성해주면된다.
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") Collection<String> names);
Hibernate:
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.tema_id as tema_id4_0_,
member0_.username as username3_0_
from
member member0_
where
member0_.username in (
? , ?
)
컬렉션을 파리미터로 주면 알아서 in 쿼리를 만들어서 준다.
spring.jpa.properties.hibernate.query.in_clause_parameter_padding=true
application properties 에서 설정을 해주면 성능을 최적화할수 있다.
이렇게 되면 execution plan을 재사용할 수 있게되어 execution plan cache가 힙을 가득채워버리는 문제도 해결이 된다. 한마디로 듬성듬성 미리 in 쿼리를 만들어놓고 남는 부분은 마지막 숫자로 패딩해서 유용하게 사용한다.
반환타입
List<Member> findMemberByUsername(String username);
Member findMemberByUsername(String username);
Optional<Member> findMemberByUsername(String username);
같은 쿼리를 만들어주더라도 반환타입에따라서 알아서 다르게 return 해준다.
하지만 단건조회일 경우 2개 이상이면 exception을 발생시킨다.
List의 경우는 없으면 그냥 empty collection이 반환이된다. null 이 아니다.
스프링 데이터 JPA 페이징과 정렬
org.springframework.data.domain.Sort = 정렬기능
org.springframework.data.domain.Pageable = 패이징 기능(내부에 Sort포함)
org.springframework.data.domain.Page= 추가 count 쿼리 결과를 포함하는 페이징
org.springframework.data.domain.Slice = 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit + 1 조회)
이게 그 유튜브처럼 쭈욱 내리면 몇개씩 계속 보여주는 방식이다.
모두다 공통적으로 정의 된 내용이다. db specific 하지 않다.
Page<Member> findByAge(int age, Pageable pageable);
Hibernate:
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.tema_id as tema_id4_0_,
member0_.username as username3_0_
from
member member0_
where
member0_.age=?
order by
member0_.username desc limit ?
2021-04-12 11:49:06.729 INFO 2040 --- [ main] p6spy : #1618195746729 | took 1ms | statement | connection 3| url jdbc:mysql://localhost:3306/spring_data_jpa?serverTimezone=UTC&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
select member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.tema_id as tema_id4_0_, member0_.username as username3_0_ from member member0_ where member0_.age=? order by member0_.username desc limit ?
select member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.tema_id as tema_id4_0_, member0_.username as username3_0_ from member member0_ where member0_.age=20 order by member0_.username desc limit 3;
Hibernate:
select
count(member0_.member_id) as col_0_0_
from
member member0_
where
member0_.age=?
반환값을 Page로 해주면 내부적으로 페이징 쿼리를 날려서 List<Membmer> 를 줄뿐만아니라 전체 갯수도 돌려준다.
@Test
public void paging() throws Exception
{
//given
Member m1 = new Member("member1", 20);
Member m2 = new Member("member2", 20);
Member m3 = new Member("member3", 20);
Member m4 = new Member("member4", 20);
Member m5 = new Member("member5", 20);
memberRepository.save(m1);
memberRepository.save(m2);
memberRepository.save(m3);
memberRepository.save(m4);
memberRepository.save(m5);
//when
int age = 20;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
//then
Page<Member> page = memberRepository.findByAge(age,pageRequest);
List<Member> content = page.getContent();
long totalElements = page.getTotalElements();
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가 제공해주는 메소드가 엄청많다.
이번엔 Slice로 받아보자
Slice<Member> findByAge(int age, Pageable pageable);
@Test
public void paging() throws Exception
{
//given
Member m1 = new Member("member1", 20);
Member m2 = new Member("member2", 20);
Member m3 = new Member("member3", 20);
Member m4 = new Member("member4", 20);
Member m5 = new Member("member5", 20);
memberRepository.save(m1);
memberRepository.save(m2);
memberRepository.save(m3);
memberRepository.save(m4);
memberRepository.save(m5);
//when
int age = 20;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
//then
Slice<Member> page = memberRepository.findByAge(age,pageRequest);
List<Member> content = page.getContent();
assertThat(content.size()).isEqualTo(3);
assertThat(page.getNumber()).isEqualTo(0);
assertThat(page.isFirst()).isTrue();
assertThat(page.hasNext()).isTrue();
}
Slice는 getTotalElements 와 getTotalPages 메소드가 없다.
Hibernate:
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.tema_id as tema_id4_0_,
member0_.username as username3_0_
from
member member0_
where
member0_.age=?
order by
member0_.username desc limit ?
2021-04-12 12:00:46.866 INFO 6776 --- [ main] p6spy : #1618196446866 | took 1ms | statement | connection 3| url jdbc:mysql://localhost:3306/spring_data_jpa?serverTimezone=UTC&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
select member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.tema_id as tema_id4_0_, member0_.username as username3_0_ from member member0_ where member0_.age=? order by member0_.username desc limit ?
select member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.tema_id as tema_id4_0_, member0_.username as username3_0_ from member member0_ where member0_.age=20 order by member0_.username desc limit 4;
쿼리를 날릴떄 요청한 limit 보다 +1 해서 쿼리해온다.
실무에서는 totalcount 자체가 성능의 문제를 일으킨다. 전체 DB를 훑어 봐야하기 때문이다.
@Query(value = "select m from Member m left join m.team t",
countQuery = "select count(m.username) from Member m")
Slice<Member> findByAge(int age, Pageable pageable);
실무에서는 countQuery를 따로 최적화해서 작성해주는 것이 좋다.
벌크성 수정 쿼리
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();
}
@Modifying
@Query("update Member m set m.age = m.age +1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
executeUpdate() 가 return 해주는것은 update가 실행된 row의 갯수다. @Modifying 이 꼭 필요하다.
벌크연산은 1차캐시를 거치지 않고 바로 db에 update 쿼리를 날리기 때문에 조심해야한다.
@EntityGraph
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
@EntityGraph(attributePaths = {"team"})
@Query("select m form Member m")
List<Member> findMemberEntityGraph();
@EntityGraph(attributePaths = {"team"})
List<Member> findEntityGraphByUsername(@Param("username") String username);
내부적으로 @EntityGraph는 join fetch를 쓰는것이다. 마지막 메소드명에서 EntityGraph 앞에 대문자만 들어가고 아무거나 써도된다.
JPA Hint & Lock
나의 경우엔 hibernate에게 알려주는 힌트이다.
@Test
public void queryHint() throws Exception
{
//given
Member member1 = memberRepository.save(new Member("member1", 10));
em.flush();
em.clear();
//when
Member findMember = memberRepository.findReadOnlyByUsername("member1");
findMember.setUsername("member2");
em.flush();
//then
}
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
flush할떄 dirty checking에 의해서 update 쿼리가 날아가는게 정상이지만
readOnly 옵션을 주었기에 따로 스냅샷을 안만들어 놓아서 dirty checking 기능이 작동을 안한다. 스냅샷이란 영속성 컨텍스트가 생성될 때 , 향후 dirty checking을 위해 원본을 복사해서 만들어두는 객체를 의미한다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findLockByUsername(String username);
여러 Lock 옵션들을제공한다.
'WEB > JPA' 카테고리의 다른 글
실전! Spring Data JPA 4,5 (스프링 데이터 JPA 분석, 나머지 기능들) (0) | 2021.04.14 |
---|---|
실전! Spring Data JPA 3(확장기능,Auditing ,Page) (0) | 2021.04.13 |
실전! 스프링부트와 JPA와 활용2 (OSIV와 성능 최적화) (0) | 2021.04.06 |
실전! 스프링부트와 JPA와 활용2 (컬렉션 조회 최적화) (0) | 2021.04.06 |
실전! 스프링 부트와 JPA 활용2 (API 개발기본 , 지연로딩과 조회 성능 최적화) (0) | 2021.04.03 |