Spring/JPA

스프링 데이터 JPA - (1)

ummchicken 2022. 12. 19. 23:30

출처 : 실전! 스프링 데이터 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 구현체들이 프록시를 강제로 만들어야 할 때...)



  • 🚨 테스트 에러 시
    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
    → JPA의 모든 데이터 변경은 @Transactional 안에서 이루어져야 한다.



  • 🚨 테스트코드 실행 시, 테이블만 만들고 등록, 수정 쿼리가 안 보일 때
    @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는 유연한 반환 타입 지원