출처 : 실전! 스프링 데이터 JPA
섹션 1. 프로젝트 환경설정
- @Setter를 넣기 보다는
package study.datajpa.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
protected Member() {
}
public Member(String username) {
this.username = username;
}
}
- @Setter를 없애고, 필요에 따라
package study.datajpa.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
@Getter
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
protected Member() {
}
public Member(String username) {
this.username = username;
}
public void changeUsername(String username) {
this.username = username;
}
}
→ 이런식으로 changeUsername(String username) 메소드를 주는 게 더 나은 방법이라고 봄
※ 참고
protected Member() {
}
가 있는 이유
→ JPA 표준 스펙에 엔티티는 기본적으로 파라미터 없는 디폴트 생성자가 있어야 한다.
→ protected까지 열어놔야 함.
(∵ JPA가 프록시 이런 기술들을 쓰는데, JPA 구현체들이 프록시를 강제로 만들어야 할 때...)
- 🚨 테스트 에러 시
→ JPA의 모든 데이터 변경은 @Transactional 안에서 이루어져야 한다.org.springframework.dao.InvalidDataAccessApiUsageException: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call; nested exception is javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call
- 🚨 테스트코드 실행 시, 테이블만 만들고 등록, 수정 쿼리가 안 보일 때
→ @Transactional가 있으면, 끝날 때 다 Rollback을 시킴
∴ 우리 눈에 보고 싶으면, @Rollback(value = false) 추가
→ 결과가 나온다.insert into member (username, id) values (?, ?)
MemberJpaRepository vs MemberRepository 비교
- MemberJpaRepository
package study.datajpa.repository;
import org.springframework.stereotype.Repository;
import study.datajpa.entity.Member;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@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);
}
}
- MemberRepository
package study.datajpa.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import study.datajpa.entity.Member;
public interface MemberRepository extends JpaRepository<Member, Long> {
}
→ 테스트 코드 실행 시, 같은 결과가 나온다.
섹션 2. 예제 도메인 모델
- JPA 표준 스펙에 엔티티는 기본적으로 파라미터 없는 디폴트 생성자가 있어야 한다.
→ protected까지 열어놔야 함.
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
protected Member() {
}
public Member(String username) {
this.username = username;
}
}
→ protected Member() {} 대신, @NoArgsConstructor(access = AccessLevel.PROTECTED) 넣기
package study.datajpa.entity;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
public Member(String username) {
this.username = username;
}
}
※ 초기화
- em.flush();
JPA em.persist를 하면 바로 DB에 insert 쿼리를 날리는 게 아님.
JPA 영속성 컨텍스트에 member와 team을 다 모아 놓음.
그리고 flush를 하면 강제로 DB에 insert 쿼리를 다 날리게 됨. - em.clear();
DB에 쿼리를 다 날리고 JPA 영속성 컨텍스트에 있는 캐시를 다 날린다.
그래서 깔끔하게 다 확인됨.
섹션 3. 공통 인터페이스 기능
※ 참고
JPA에서 수정은 변경감지 기능을 사용하면 된다.
트랜잭션 안에서 엔티티를 조회한 다음에 데이터를 변경하면,
트랜잭션 종료 시점에 변경감지 기능이 작동해서
변경된 엔티티를 감지하고 UPDATE SQL을 실행한다.
주요 메서드
- save(S): 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
- delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
- findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
- getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
- findAll(…) : 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다.
→ 내가 상상할 수 있는 공통 기능들은 다 제공한다!
→ 도메인에 특화된 기능들은 어떻게 해야할까? 공통으로 만드는 게 불가한... : 커스텀 기능
(ex. List<Member> findByUsername(String username);)
→→ 구현하지 않아도 동작한다!! : 쿼리 메소드 기능
섹션 4. 쿼리 메소드 기능
1. 메소드 이름으로 쿼리 생성
스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행
이름과 나이를 기준으로 회원을 조회하려면?
- MemberJpaRepository.java
// 이름과 나이를 기준으로 회원을 조회하려면?
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();
}
- MemberRepository.java
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
→ 동일하게 동작한다!
- findByUsernameAndAgeGreaterThan: 이름의 관례로 인해... 이름 바꾸면 동작 안 함
where
member0_.username=?
and member0_.age>?
2. JPA NamedQuery
JPA의 NamedQuery를 호출할 수 있음
※ 이 기능은 거의 실무에서 쓸 일이 없음
스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다.
대신 @Query 를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다.
3. @Query, 리포지토리 메소드에 쿼리 정의하기
⭐ 권장하는 기능
- MemberRepository.java
@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);
- JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음(매우 큰 장점!)
- 동적쿼리는 Querydsl 써라
4. @Query, 값, DTO 조회하기
단순히 값 하나를 조회 (여태까지는 엔티티 타입만 조회한 것임)
- MemberRepository.java
// 사용자의 이름 리스트만 다 가져오고 싶을 때
@Query("select m.username from Member m")
List<String> findUsernameList();
5. 파라미터 바인딩
- 위치 기반
- 이름 기반 (코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자)
6. 컬렉션 파라미터 바인딩
Collection 타입으로 in절 지원
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
→ 결과
where
member0_.username in (
? , ?
)
7. 반환 타입
스프링 데이터 JPA는 유연한 반환 타입 지원
'Spring > JPA' 카테고리의 다른 글
[JPA] MyBatis와 JPA, 도대체 뭐가 다를까? (0) | 2023.02.06 |
---|---|
스프링 데이터 JPA - (2) (0) | 2022.12.20 |
JPA 활용 2 - API 개발과 성능 최적화 (0) | 2022.12.19 |
JPA 강의 8 - 객체지향 쿼리 언어 2, JPA N + 1 문제 (중급 문법) (0) | 2022.12.19 |
JPA 강의 7 - 객체지향 쿼리 언어 1 (기본 문법) (0) | 2022.12.19 |