순수 JPA 기반 리포지토리 만들기
- 순수 JPA 기반 리포지토리를 만들 것이다.
- 기본 CRUD
- 저장
- 변경 - 변경감지 사용
- 삭제
- 전체 조회
- 단건 조회
- 카운트
참고로 JPA에서 수정은 변경감지 기능을 사용하면 된다. 트랜잭션안에서 엔티티를 조회한 다음에 데이터를 변경하면, 트랜잭션 종료 시점에 변경감지 기능이 작동해서 변경된 엔티티를 감지하고 update sql 쿼리가 실행된다.
1. 순수 JPA 기반 리포지토리 - Member
📌 MemberJpaRepository
package study.datajpa.repository;
@Repository
public class MemberJpaRepository {
@PersistenceContext
private EntityManager em;
public Member save(Member member) {
em.persist(member);
return member;
}
public void delete(Member member) {
em.remove(member);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.of(member);
}
public long count() {
return em.createQuery("select count(m) from Member m", Long.class)
.getSingleResult();
}
public Member find(Long id) {
return em.find(Member.class, id);
}
}
2. 순수 JPA 기반 리포지토리 - Team
📌 TeamJpaRepository
package study.datajpa.repository;
@Repository
@RequiredArgsConstructor
public class TeamJpaRepository {
private final EntityManager em;
public Team save(Team team) {
em.persist(team);
return team;
}
public void delete(Team team) {
em.remove(team);
}
public List<Team> findAll() {
return em.createQuery("select t from Team t", Team.class)
.getResultList();
}
public Optional<Team> findById(Long id) {
Team team = em.find(Team.class, id);
return Optional.of(team);
}
public Long count() {
return em.createQuery("select count(t) from Team t", Long.class)
.getSingleResult();
}
}
3. 순수 JPA 기반 리포지토리 테스트
package study.datajpa.repository;
@SpringBootTest
@Transactional
@Rollback(false)
class MemberJpaRepositoryTest {
@Autowired MemberJpaRepository memberJpaRepository;
@Test
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);
}
@Test
public void basicCRUD() {
Member member1 = new Member("member1");
Member member2 = new Member("member2");
memberJpaRepository.save(member1);
memberJpaRepository.save(member2);
// 단건 조회 검증
Member findMember1 = memberJpaRepository.findById(member1.getId()).orElse(null);
Member findMember2 = memberJpaRepository.findById(member2.getId()).orElse(null);
assertThat(findMember1).isEqualTo(member1);
assertThat(findMember2).isEqualTo(member2);
// update 쿼리가 나간다. (변경감지)
findMember1.setUsername("member!!!");
// 리스트 조회 검증
List<Member> all = memberJpaRepository.findAll();
assertThat(all.size()).isEqualTo(2);
// 카운트 검증
long count = memberJpaRepository.count();
assertThat(count).isEqualTo(2);
// 삭제 검증
memberJpaRepository.delete(member1);
memberJpaRepository.delete(member2);
long deletedCount = memberJpaRepository.count();
assertThat(deletedCount).isEqualTo(0);
}
}
- 기본 CRUD를 검증한다.
여기서 findMember1.setUsername() 메서드를 통해 변경 감지로 수정하였다.
update
member
set
age=?,
team_id=?,
username=?
where
member_id=?
update 쿼리 나간 것을 보면 username 필드 값만 변경했는데 Member 엔티티의 모든 필드값인 age, team_id, username 의 updatq 쿼리문이 나갔다.
JPA에서 변경된 필드만 update 하지 않고 모든 필드를 변경하는 쿼리를 생성하는 이유는 모든 필드를 update 쿼리로 만들면 수정 쿼리가 항상 동일하게 만들어지므로 미리 update 쿼리를 생성하여 재사용이 가능하기 때문이다.
또한 데이터베이스는 동일한 쿼리를 전달하면 한 번 파싱된 쿼리를 재사용할 수 있다.
이러한 이점 때문에 JPA는 모든 필드를 updatq 쿼리로 만들어서 실행한다.
하지만 변경된 필드만 update 쿼리를 적용하고 싶다면 해당 엔티티의 클래스레벨에 @DynamicUpdate 애노테이션을 사용하면 된다.
대략 칼럼 개수가 30개 이상되면 정적 수정 쿼리보다 @DynamicUpdate 애노테이션을 사용한 동적 수정 쿼리가 더 빠르다.
하지만 한 테이블에 칼럼이 30개 이상 된다는 것은 테이블 설계상 책임이 적절하게 분리되지 않았을 가능성이 더 크다.
공통 인터페이스 설정 - JpaRepository
1. JavaConfig 설정 - 스프링 부트 사용시 생략 가능
package study.datajpa;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
//@EnableJpaRepositories(basePackages = "study.datajpa.repository") // 스프링 부트 사용시 생략 가능
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
}
스프링 부트를 사용하지 않을 경우 @EnableJpaRepositories 애노테이션으로 패키지 지정을 해줘야하는데 스프링 부트에서는 @SpringBootApplication 애노테이션이 붙은 클래스 기준으로 하위 패키지는 전부 적용되도록 기본 설정이 되어있다.
2. 스프링 데이터 JPA가 리포지토리의 구현 클래스를 대신 생성한다.
Spring Data Jpa가 JpaRepository 인터페이스를 상속받는 리포지토리 인터페이스들의 구현 클래스를 생성해준다.
또한, JpaRepository 인터페이스를 상속 받는 리포지토리 인터페이스는 @Repository 애노테이션을 생략 가능하다.
📌 MemberRepository - 멤버 리포지토리
package study.datajpa.repository;
public interface MemberRepository extends JpaRepository<Member, Long> {
}
📌 TeamRepository - 팀 리포지토리
package study.datajpa.repository;
@Repository
public interface TeamRepository extends JpaRepository<Team, Long> {
}
🔍 리포지토리 출력하기
@Test
void name() {
System.out.println("memberRepository = " + memberRepository.getClass());
}
memberRepository = class com.sun.proxy.$Proxy117
스프링이 인터페이스를 보고 proxy 객체로 구현체를 만들어서 주입시켰다.
출력 결과에 멤버 리포지토리의 구현체가 프록시 객체로 나온다.
📌 MemberRepositoryTest 테스트 코드
package study.datajpa.repository;
@SpringBootTest
@Transactional
@Rollback(false)
class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
@Test
void name() {
System.out.println("memberRepository = " + memberRepository.getClass());
}
@Test
void testMember() {
Member member = new Member("MemberA");
Member savedMember = memberRepository.save(member);
Member findMember = memberRepository.findById(savedMember.getId()).orElse(null);
assertThat(findMember.getId()).isEqualTo(member.getId());
assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
assertThat(findMember).isEqualTo(member);
}
@Test
public void basicCRUD() {
Member member1 = new Member("member1");
Member member2 = new Member("member2");
memberRepository.save(member1);
memberRepository.save(member2);
// 단건 조회 검증
Member findMember1 = memberRepository.findById(member1.getId()).orElse(null);
Member findMember2 = memberRepository.findById(member2.getId()).orElse(null);
assertThat(findMember1).isEqualTo(member1);
assertThat(findMember2).isEqualTo(member2);
// update 쿼리가 나간다. (변경감지)
findMember1.setUsername("member!!!");
// 리스트 조회 검증
List<Member> all = memberRepository.findAll();
assertThat(all.size()).isEqualTo(2);
// 카운트 검증
long count = memberRepository.count();
assertThat(count).isEqualTo(2);
// 삭제 검증
memberRepository.delete(member1);
memberRepository.delete(member2);
long deletedCount = memberRepository.count();
assertThat(deletedCount).isEqualTo(0);
}
}
JpaRepository 인터페이스 기능을 확인하기 위해 CRUD 테스트 코드를 작성했다.
공통 인터페이스 분석
JpaRepository 인터페이스는 공통 CRUD 기능을 제공한다.
1. 공통 인터페이스 구성
스프링 데이터에 속해있는 인터페이스들은 NoSql, Sql을 모두 지원하는 공통 인터페이스다.
스프링 데이터 JPA에 속해있는 인터페이스들은 JPA를 지원하는 지원하는 공통 인터페이스다.
참고) 제네릭 타입
T : 엔티티
ID : 엔티티의 식별자 타입
S : 엔티티와 그 자식 타입
2. 주요 메서드
- save(s) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
- delete(T) : 엔티티 하나를 삭제한다.
- 내부에서 EntityManager.remove() 호출
- findById(ID) : 엔티티 하나를 조회한다.
- 내부에서 EntityManager.find() 호출
- getOneId() : 엔티티를 프록시로 조회한다.
- 내부에서 EntityManager.getReference() 호출
- findAll(...) : 모든 엔티티를 조회한다.
- 정렬(Sort)이나 페이징(Pageable) 조건을 파라미터로 제공할 수 있다.
👀 참고 자료
실전! 스프링 데이터 JPA - 인프런 | 강의
스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.
www.inflearn.com
https://small-stap.tistory.com/74?category=976123
[JPA] Dirty Checking(변경 감지)
이 글은 혼자 학습한 내용을 바탕으로 작성되었습니다. 틀리거나 잘못된 정보가 있을 수 있습니다. 댓글로 알려주시면 수정하도록 하겠습니다. 1. JPA Dirty Checking JPA에는 수정과 관련된 메소드가
small-stap.tistory.com
'[JPA] > 실전! 스프링 데이터 JPA' 카테고리의 다른 글
[JPA] 나머지 기능들 (0) | 2022.04.22 |
---|---|
스프링 데이터 JPA 분석 (0) | 2022.04.21 |
[JPA] 확장 가능 (0) | 2022.04.20 |
[JPA] 쿼리 메소드 기능 (0) | 2022.04.17 |
[JPA] 예제 도메인 모델 (0) | 2022.04.16 |