아 이걸 비교하려면 두 코드를 옆에 대놓고 편하게 눈만 굴려 왔다갔다하면서 비교해야하는데.. 벨로그는 단을 두개로 쓸 수 있는 기능
이 없어서 아쉽다. 네이버웨일이나 크롬을 쓰신다면
듀얼탭 기능을 통해 보면 도움이 된다. (웨일추천)
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
...
}
@Repository
public class MemberJpaRepository {
@PersistenceContext
private EntityManager em;
public Member save(Member member) {
em.persist(member);
return member;
}
public Member find(Long id) {
return em.find(Member.class, id);
}
}
@SpringBootTest
@Transactional
@Rollback(false)
public class MemberJpaRepositoryTest {
@Autowired
MemberJpaRepository memberJpaRepository;
@Test
public void testMember() {
Member member = new Member("memberA");
Member savedMember = memberJpaRepository.save(member);
Member findMember = memberJpaRepository.find(savedMember.getId());
assertThat(findMember.getId()).isEqualTo(member.getId());
assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
assertThat(findMember).isEqualTo(member); //JPA 엔티티 동일성 보장
}
}
public interface MemberRepository extends JpaRepository<Member, Long> {
}
@SpringBootTest
@Transactional
@Rollback(false)
public class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Test
public void testMember() {
Member member = new Member("memberA");
Member savedMember = memberRepository.save(member);
Member findMember =
memberRepository.findById(savedMember.getId()).get();
Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername())
;
Assertions.assertThat(findMember).isEqualTo(member); //JPA 엔티티 동일성
보장
}
}
인터페이스인 주제에 어떻게 MemberRepository
는 구현체 없이 바로 메소드를 실행시킬 수 있는 걸까? 그것은 상위 클래스인 JpaRepository
가 프록시 객체로 구현 클래스를 직접 만들어서 MemberRepository
를 사용하는 곳에 넣어주기 때문이다.
그림을 가지고 이해해보자. 내가 ItemRepository
를 인터페이스로 만들었고 Spring Data Jpa를 상속 받았다고 하자. 그럼 내가 ItemRepository
를 의존성 주입하는 클래스 내에서 Spring Data Jpa
가 프록시 객체를 통해 구현 클래스를 만들어서 주입해준다.
public interface JpaRepository<T, ID extends Serializable>
extends PagingAndSortingRepository<T, ID>
{
...
}
JpaRepository
인터페이스: 공통 CRUD 제공JpaRepository
를 사용하는 인터페이스public interface MemberRepository extends JpaRepository<Member, Long> {
}
주의: T findOne(ID)
→ Optional<T> findById(ID)
변경
제네릭 타입
T
: 엔티티ID
: 엔티티의 식별자 타입S
: 엔티티와 그 자식 타입주요 메서드
save(S)
: 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합(merge)한다.delete(T)
: 엔티티 하나를 삭제한다. 내부에서 EntityManger.remove()
호출findById(ID)
: 엔티티 하나를 조회한다. 내부에서 EntityManager.find()
호출getOne(ID)
: 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()
호출findALl(...)
: 모든 엔티티를 조회한다. 정렬(Sort
)이나 페이징(Pageable
)조건을 파라미터로 제공할 수 있다.NamedQuery
@Query
리포지토리 메소드에서 쿼리 정의스프링 데이터 JPA는 메소드 이름을 분석해서 마법처럼 JPQL 쿼리를 실행한다.
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
COUNT
: count ... By 반환타입 long
EXISTS
: exists...By 반환타입 boolean
long
DISTINCT
: findDistinct, findMemberDistinctByLIMIT
: findFirst3, findFIrst, findTop, findTop3이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다. 이렇게 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점이다.
이 내용은 실무에서 쓸 일이 거의 없다.
@NamedQuery
어노테이션으로 Named 쿼리 정의@Entity
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username"
)
public class Member {
...
}
public class MemberRepository {
public List<Member> findByUsername(String username) {
...
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", username)
.getResultList();
}
}
NamedQuery
사용@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
@Query
를 생략하고 메서드 이름만으로 Named 쿼리를 호출할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long> { //** 여기 선언한 Member 도메인 클래스
List<Member> findByUsername(@Param("username") String username);
}
참고
스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다.
대신@Query
를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다.
@Query
, 리포지토리 메소드에 쿼리 정의하기실무에서 많이 쓰는 기능
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);
}
@org.springframework.data.jpa.repository.Query
어노테이션을 사용참고
실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 매우 지저분해진다. 따라서@Query
기능을 자주 사용하게 된다.
@Query
를 통해 정의한 쿼리는 사실상 이름이 없는 @NamedQuery
라고 보면 된다. 애플리케이션 로딩 시점에 파싱해버린다. 근데 파싱 시점에 문법 오류가 있으면 이걸 반환해준다. 스앵님도 권장하는 기능.
간단한 기능을 가진 메소드를 쓴다면 2.1의 메소드 이름으로 쿼리 생성 기능을 쓰고 복잡해진다면 2.3의 @Query
를 쓰고 메소드 이름을 단순하게 가져가는 것을 권한다.
실무에서 많이 사용하는 기능
@Query("select m.username from Member m")
List<String> findUsernameList();
@Query("select new study.datajpa.repository.MemberDto(m.id, m.username, t.name) "
+ "from Member m join m.team t")
List<MemberDto> findMemberDto();
주의
DTO로 직접 조회하려면 JPA의new
명령어를 사용해야 한다. 그리고 다음과 같이 생성자가 맞는 DTO가 필요하다. (JPA와 사용방식이 동일하다.)
package study.datajpa.repository;
import lombok.Data;
@Data
public class MemberDto {
private Long id;
private String username;
private String teamName;
public MemberDto(Long id, String username, String teamName) {
this.id = id;
this.username = username;
this.teamName = teamName;
}
}
코드 가독성, 유지보수성 면에서 이름 기반이 위치 기반보다 우수하다.
select m from Member m where m.username =? 1 //위치 기반
select m from Member m where m.username = :name //이름 기반
import org.springframework.data.repository.query.Param
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = :name")
Member findMembers(@Param("name") String username);
}
참고
코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자 (위치기반은 순서 실수가
바꾸면…)
Collection
타입으로 in절 지원
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
스프링 데이터 JPA는 유연한 반환 타입을 지원한다.
List<Member> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional
null
반환javax.persistence.NonUniqueResultException
예외 발생참고
단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의Query.getStringResult()
메서드를 호출한다. 이 메서드를 호출했을 때 조회 결과가 없으면javax.persistence.NonUniqueResultException
예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다. 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에null
을 반환한다.
우리가 개발할 때 DB안에 있는 수만 가지 데이터들을 한번에 퍼올릴 수가 없다. 적당한 사이즈로 끊어서 데이터를 끌어와야하는데 그걸 정렬(sorting)된 상태로 적절한 데이터를 끊어서 가져와서 API나 화면으로 전달할 때 꼭 필요한 기능이 페이징
과 정렬
이다.
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();
}
public long totalCount(int age) {
return em.createQuery("select count(m) from Member m where m.age = :age",
Long.class)
.setParameter("age", age)
.getSingleResult();
}
@Test
public void paging() throws Exception {
//given
memberJpaRepository.save(new Member("member1", 10));
memberJpaRepository.save(new Member("member2", 10));
memberJpaRepository.save(new Member("member3", 10));
memberJpaRepository.save(new Member("member4", 10));
memberJpaRepository.save(new Member("member5", 10));
int age = 10;
int offset = 0;
int limit = 3;
//when
List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
long totalCount = memberJpaRepository.totalCount(age);
//페이지 계산 공식 적용...
// totalPage = totalCount / size ...
// 마지막 페이지 ...
// 최초 페이지 ..
//then
assertThat(members.size()).isEqualTo(3);
assertThat(totalCount).isEqualTo(5);
}
org.springframework.data.domain.Sort
: 정렬 기능org.springframework.data.domain.Pageable
: 페이징 기능 (내부에 Sort 포함)org.springframework.data.domain.Page
: 추가 count 쿼리 결과를 포함하는 페이징org.springframework.data.domain.Slice
: 추가 count 쿼리 없이 다음 페이지만 확인 가능List
(자바 컬렉션): 추가 count 쿼리 없이 결과만 반환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);
public interface MemberRepository extends Repository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
//페이징 조건과 정렬 조건 설정
@Test
public void page() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 10));
memberRepository.save(new Member("member3", 10));
memberRepository.save(new Member("member4", 10));
memberRepository.save(new Member("member5", 10));
//when
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC,
"username"));
Page<Member> page = memberRepository.findByAge(10, pageRequest);
/*
스프링 데이터 JPA는 totalCount가 따로 필요가 없다.
반환 타입을 Page로 받으면 이 놈이 totalCount 쿼리를 같이 날린다.
*/
//then
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(); //다음 페이지가 있는가?
}
Pagable
은 인터페이스다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한org.springframework.data.domain.PageRequest
객체를 사용한다.PageRequest
생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}
public interface Slice<T> extends Streamable<T> {
int getNumber(); //현재 페이지
int getSize(); //페이지 크기
int getNumberOfElements(); //현재 페이지에 나올 데이터 수
List<T> getContent(); //조회된 데이터
boolean hasContent(); //조회된 데이터 존재 여부
Sort getSort(); //정렬 정보
boolean isFirst(); //현재 페이지가 첫 페이지 인지 여부
boolean isLast(); //현재 페이지가 마지막 페이지 인지 여부
boolean hasNext(); //다음 페이지 여부
boolean hasPrevious(); //이전 페이지 여부
Pageable getPageable(); //페이지 요청 정보
Pageable nextPageable(); //다음 페이지 객체
Pageable previousPageable();//이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}
@Query(value = “select m from Member m”,
countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
JPA는 엔티티를 가지고 와서 데이터를 변경하면 Dirty checking
을 한다. 다시 말하면, 엔티티를 조회해서 값을 바꾸면 트랜잭션 시점에 변경 감지를 통해 update
쿼리를 날린다. 이것은 한 건에 하나씩 하는 것이다.
한 건에 하나씩 쿼리를 날려주는 것이 아닌 예를 들어서, 모든 직원의 연봉을 10%씩 인상한다.
라는 명령을 수행해야 할때는 한 건씩 하는 것보다 DB에다가 Update쿼리에 *10%
를 붙여서줘 한번에 커밋하는 것이 훨씬 효율적이다. 이런 것들을 JPA에서는 벌크성 수정 쿼리라고 한다.
JPA에선 엔티티의 중심이 객체이기 때문에 SQL에서 하는 쿼리와 달리 분리가 된다.
public int bulkAgePlus(int age) {
int resultCount = em.createQuery(
"update Member m set m.age = m.age + 1" +
"where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
return resultCount;
}
@Test
public void bulkUpdate() throws Exception {
//given
memberJpaRepository.save(new Member("member1", 10));
memberJpaRepository.save(new Member("member2", 19));
memberJpaRepository.save(new Member("member3", 20));
memberJpaRepository.save(new Member("member4", 21));
memberJpaRepository.save(new Member("member5", 40));
//when
int resultCount = memberJpaRepository.bulkAgePlus(20);
//then
assertThat(resultCount).isEqualTo(3);
}
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
@Test
public void bulkUpdate() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));
//when
int resultCount = memberRepository.bulkAgePlus(20);
//then
assertThat(resultCount).isEqualTo(3);
}
@Modifying
어노테이션을 사용org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations
@Modifying(clearAutomatically = true)
(이 옵션은 기본값이 true
)findById
로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있다. 만약 다시 조회해야 한다면 꼭 영속성 컨텍스트를 초기화하자.참고
벌크성 수정쿼리를 사용하기 위해선 주의해야할 점이 있다. JPA라는 것은 영속성 컨텍스트라는 것에서 엔티티가 전부 관리된다. 이 벌크 연산은 이런 것을 무시하고 DB에 바로 쿼리를 날린다. 그렇기 때문에 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.
권장하는 방안
1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
2. 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.
@EntityGraph
연관된 엔티티들을 SQL 한번에 조회하는 방법
member
-> team
은 지연로딩 관계이다. 따라서 다음과 같이 team의 데이터를 조회할 때마다 쿼리가 실행된다. (N + 1문제 발생)
@Test
public void findMemberLazy() throws Exception {
//given
//member1 -> teamA
//member2 -> teamB
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
teamRepository.save(teamA);
teamRepository.save(teamB);
memberRepository.save(new Member("member1", 10, teamA));
memberRepository.save(new Member("member2", 20, teamB));
em.flush();
em.clear();
//when
List<Member> members = memberRepository.findAll();
//then
for (Member member : members) {
member.getTeam().getName();
}
}
List<Member> members = memberRepository.findAll();
이 부분에서 member
만 DB에서 가지고 온다. member
클래스 안에 필드에는 id
, username
, age
, team
등의 값들이 다 들어있다. 하지만 Member
클래스와 Team
클래스가 일대다 관계로 연관관계 매핑이 되어있는 경우, DB의 Team
테이블까지 건드린다. 굳이 DB 안에서 Team
테이블이 필요하지 않는데 괜히 한 가지의 테이블을 더 건드리면 그만큼 컴퓨터의 리소스를 비효율적으로 사용하고 있다는 뜻이 된다. 가져와야 하는 데이터의 크기가 커질수록 우리는 컴퓨터를 함부로 대하는 악덕 개발자가 된다.
따라서 굳이 Member
테이블 데이터에서 Team
이 필요한 것이 아니라면 지연로딩을 걸어 Team
테이블까지 가져온 척하는 것이다.
지연로딩은 Proxy라는 가짜 객체를 만들어서 그 가짜 객체를 가져오는 것이다.
그러다가 정말 Member
테이블 안에서 Team
의 데이터가 필요하게 되는 경우 프록시 객체는 영속성 컨텍스트를 통해 DB에서 테이블을 가져와 실제 엔티티를 생성하고 프록시 객체를 연결시킨다. 즉, member.getTeam().getName()
처럼 member
클래스 안에서 team
의 데이터가 필요해서 가지고 올 때 DB에 Team
테이블의 데이터를 가져오는 쿼리를 날린다.
//Hibernate 기능으로 확인
Hibernate.isInitialized(member.getTeam())
//JPA 표준 방법으로 확인
PersistenceUnitUtil util =
em.getEntityManagerFactory().getPersistenceUnitUtil();
util.isLoaded(member.getTeam());
그런데 정말 member
에서 team
이 필요한 경우에만 같이 끌고 온다고 해도, member
쿼리와 team
쿼리 두 번을 날려 한번 코드를 날릴 때 두번의 쿼리가 날라가는 것이 효율적이라고 말할 수 있을까. 앞서 지연로딩으로 어느정도 보완을 해줬다고 해도, 이 한 번의 코드에 두번의 쿼리(N + 1) 문제는 여전히 성능문제를 야기시킨다. 애초부터 한번의 쿼리로 조회할 수 있으면 좋다.
그때 쓰는 것이 페치 조인이다. 연관된 엔티티를 한번에 조회하려면 페치 조인이 필요하다.
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하도록 도와준다. 이 기능을 사용하면 JPQL 없이 페치 조인을 사용할 수 있다. (JPQL + 엔티티 그래프 가능)
//공통 메서드 오버라이드
@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)
@NamedEntityGraph(name = "Member.all", attributeNodes =
@NamedAttributeNode("team"))
@Entity
public class Member {}
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
JPA 쿼리를 날릴 때 JPA 구현체(hibernate)한테 알려주는 힌트.
JPA 쿼리 힌트(SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트)
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value =
"true"))
Member findReadOnlyByUsername(String username);
@Test
public void queryHint() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
em.flush();
em.clear();
//when
Member member = memberRepository.findReadOnlyByUsername("member1");
member.setUsername("member2");
em.flush(); //Update Query 실행X
}
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly",
value = "true")},
forCounting = true)
Page<Member> findByUsername(String name, Pagable pageable);
org.springframework.data.jpa.repository.QueryHints
어노테이션을 사용forCounting
: 반환 타입으로 Page
인터페이스를 적용하면 추가로 호출되는 페이징을 위한 count 쿼리도 쿼리 힌트 적용(기본값 true
)@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
org.springframework.data.jpa.repository.Lock
어노테이션을 사용