실전! 스프링 데이터 JPA을 듣고 정리한 내용입니다.
순수 JPA 기반 리포지토리를 만들어보고 스프링 데이터 JPA를 적용해보자!
✏️ 기본 CRUD
- 저장
- 변경 → 변경감지 사용
- 삭제
- 전체 조회
- 단건 조회
- 카운트
💡 참고
- JPA에서 수정은 변경감지 기능을 사용하면 된다.
- 트랜잭션 안에서 엔티티를 조회한 다음에 데이터를 변경하면, 트랜잭션 종료 시점에 변경감지 기능이 작동해서 변경된 엔티티를 감지하고
UPDATE SQL
을 실행한다.
package study.datajpa.repository;
import org.springframework.stereotype.Repository;
import study.datajpa.entity.Member;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
import java.util.Optional;
@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.ofNullable(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);
}
}
getSingleResult()
: 단일인 경우getResultList()
: List형인 경우
(1) Create (저장)
public Member save(Member member) {
em.persist(member);
return member;
}
(2) Read (조회)
//전체 조회
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.ofNullable(member);
}
(3) Update (변경) → 변경감지 사용
JPA는 기본적으로 엔티티 변경감지로 인해 데이터를 바꾼다!
트랜잭션 안에서 엔티티를 조회한 다음에 데이터를 변경하면, 트랜잭션 종료 시점에 변경 감지 기능이 작동해서 변경된 엔티티를 감지하고 UPDATE SQL
을 실행한다.
즉, 따로 UPDATE
문을 구현할 필요가 없다.
(4) DELETE (삭제)
public void delete(Member member) {
em.remove(member);
}
(5) Count (카운트)
public long count() {
return em.createQuery("select count(m) from Member m", Long.class)
.getSingleResult();
}
package study.datajpa.repository;
import org.springframework.stereotype.Repository;
import study.datajpa.entity.Team;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
import java.util.Optional;
@Repository
public class TeamJpaRepository {
@PersistenceContext
private 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.ofNullable(team);
}
public long count() {
return em.createQuery("select count(t) from Team t",Long.class)
.getSingleResult();
}
}
(1) Create (저장)
public Team save(Team team) {
em.persist(team);
return team;
}
(2) Read (조회)
//전체 조회
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.ofNullable(team);
}
(3) Update (변경) → 변경 감지 사용
A.에서 언급했듯이, Update문
을 구현하지 않아도 된다!
(4) Delete (삭제)
public void delete(Team team) {
em.remove(team);
}
(5) Count (카운트)
public long count() {
return em.createQuery("select count(t) from Team t", Long.class)
.getSingleResult();
}
💡 참고
Member
와Team
의Repository 코드
가 매우 유사하다는 것을 알 수 있다.- 이러한 중복을 스프링 데이터 JPA를 사용하면 해결할 수 있다.
@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()).get();
Member findMember2 = memberJpaRepository.findById(member2.getId()).get();
assertThat(findMember1).isEqualTo(member1);
assertThat(findMember2).isEqualTo(member2);
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 deleteCount = memberJpaRepository.count();
assertThat(deleteCount).isEqualTo(0);
}
스프링 부트 사용시
@SpringBootApplication
위치를 지정하면, 해당 패키지와 하위 패키지를 인식한다.
✔️ 스프링 데이터 JPA가 구현 클래스 대신 생성
org.springframework.data.repository.Repository
를 구현한 클래스는 스캔 대상이다.
이는 MemberRepository
인터페이스가 동작한 이유이다.
interface
만 선언해주면 스프링 데이터 JPA
가 자동으로 구현 클래스를 만들어준다. memberRepository.getClass()
→ class com.sun.proxy.$ProxyXXX
인터페이스 구현체는 스프링 데이터 jpa가 만들어서 memberRepository
에 inject 해준다.
@Repository //생략 가능
public interface TeamRepository extends JpaRepository<Team, Long> {
}
@Repository
애노테이션은 생략 가능하다.
순수 JPA로 구현한
MemberJpaRepository
대신에 스프링 데이터 JPA가 제공하는 공통 인터페이스 사용한다.
@Repository //생략 가능
public interface TeamRepository extends JpaRepository<Team, Long> {
}
JpaRepository<(1), (2)>
MemberRepositoryTest
에 추가
@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()).get();
Member findMember2 = memberRepository.findById(member2.getId()).get();
assertThat(findMember1).isEqualTo(member1);
assertThat(findMember2).isEqualTo(member2);
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 deleteCount = memberRepository.count();
assertThat(deleteCount).isEqualTo(0);
}
MemberJpaRepositoryTest
에서 사용한 basicCRUD
복사해서 memberRepositoryTest
에 붙여넣는다.
MemberJpaRepository
가 MemberRepository
와 거의 유사하게 구현되어 있다.
실행 결과
왠만한 공통 기능은 다 제공한다.
JpaRepository
인터페이스 : 공통 CRUD 제공
✔️ JpaRepository
공통 기능 인터페이스
✔️ PagingAndSortingRepository
✔️ CrudRepository
✔️ Repository
스프링 데이터는(기본적인 기능) 어디에서든지 공통으로 사용된다. (SQL, NoSQL 상관없이)
💡 참고
T findOne(ID)
→Optional<T> findById(ID)
변경 되었다.
✔️ 제네릭 타입
T
: 엔티티ID
: 엔티티의 식별자 타입S
: 엔티티와 그 자식 타입
✔️ 주요 메서드
save(S)
: 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.delete(T)
: 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()
호출findById(ID)
: 엔티티 하나를 조회한다. 내부에서 EntityManager.find()
호출getOne(ID)
: 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()
호출 findAll(...)
: 모든 엔티티를 조회한다. 정렬( Sort
)이나 페이징( Pageable
) 조건을 파라미터로 제공할 수 있다.
💡 참고
JpaRepository
는 대부분의 공통 메서드를 제공한다.
✔️ 만약 공통 메서드(기능)이 아닌, 다른 특수의 메서드(기능)을 사용하고 싶을 때
findByUsername메서드를 구현하고 싶다.
- 약간 특성을 바꾸어서 구현해보려고 한다.
findByUsername
메서드를 구현하고 싶다.
MemberRepositoryImpl
에서 MemberRepository
를 상속 후이걸 해결하는 방법은 나중에 배운다.
이와 유사하게 구현하는 방법이 있는데 그것을 커스텀 구현이라고 한다.
✔️ 그런데?
그런데, 이와 같이 interface
에 메서드 이름만 새로 추가하고 (상속받은 클래스에서)본체를 직접 구현하지 않고 메소드를 바로 사용해도 동작을 한다!
➡ 이것은 무슨 기능일까? 쿼리메소드 기능이다.