Spring/jpa

[JPA] 스프링 데이터 JPA

라임온조 2023. 2. 23. 11:14

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/

 

Spring Data JPA - Reference Documentation

Example 119. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

1. 공통 인터페이스 기능

1) 개념

간단한 CRUD 기능을 공통으로 처리하는 JpaReository 인터페이스를 제공

2) 사용

  • JpaRepository 인터페이스 상속
  • 제네릭에 엔티티 클래스와 엔티티 클래스가 사용하는 식별자 타입 지정
public interface MemberRepository extends JpaRepository<Member, Long>{
}

3) 특징

JpaRepository 인터페이스 계층 구조

스프링 데이터(스프링 데이터 프로젝트가 공통으로 사용하는 인터페이스)
<Interface> Repository
<Interface> CrudRepository
<Interface> PagingAndSortingRepository
스프링 데이터 JPA(스프링 데이터에 있는 인터페이스에 추가로 JPA에 특화된 기능을 제공)
JpaRepository

4) 주요 메소드

save(S)

  • 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정
  • 엔티티에 식별자 값이 없으면 새로운 엔티티로 판단해서 EntityManage.persist()를 호출하고, 식별자 값이 있으면 이미 있는 엔티티로 판단해서 EntityManage.merge()를 호출

delete(T)

  • 엔티티 하나를 삭제
  • 내부에서 EntityManage.revmoe()를 호출

findAll(...)

  • 모든 엔티티를 조회
  • 정렬이나 페이징 조건을 파라미터로 제공할 수 있음

2. 쿼리 메소드 기능

1) 개념

메소드 이름만으로 쿼리를 생성하는 기능으로, 인터페이스에 메소드만 선언하면 해당 메소드의 이름으로 적절한 JPQL 쿼리를 생성해서 실행

2) 종류

메소드 이름으로 쿼리 생성

스프링 데이터 JPA 공식 문서에서 제공하는 규칙에 따라 메소드 이름을 지정하면 메소드 이름을 분석해서 JPQL 생성하고 실행

public interface MemberRepository extends Repository<Member, Long> {
	List<Member> findByEmailAndName(String email, String name);
}

// -> select m from Member m where m.email = ?1 and m.name = 2?

JPA NamedQuery

  • 쿼리에 이름을 부여해서 사용하는 방법
  • 엔티티를 정의해 놓은 클래스에 어노테이션을 이용해서 쿼리 정의
  • 스프링 데이터 JPA는 Named 쿼리를 찾아서 실행하려고 하는데, 만약 없으면 메소드 이름으로 쿼리 생성 전략을 사용
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username")

public class Member{
}
public interface MemberRepository extends JpaRepository<Member, Long>{
	List<Member> findByUsername(@Param("username") String username);
}

@Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의

  • 리포지토리 메소드에 직접 쿼리 정의
  • 실행할 메소드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다
  • 네이티브 SQL을 사용하려면 @Query 어노테이션에 nativeQuery = true를 설정한다
public interface MemberRepository extends JpaRepository<Member, Long>{
	@Query("select m from Member m where m.username = ?1")
    Member findByUsername(String username);
}

public interface MemberRepository extends JpaRepository<Member, Long>{
	@Query("SELECT * FROM MEMBER WHERE USERNAME = ?0", nativeQuery = true)
    Member findByUsername(String username);
}

3) 특징

파라미터 바인딩

select m from Member m where m.username = ?1; // 위치 기반
select m from Member m where m.username = :name; // 이름 기반
  • 기본값은 위치 기반
  • 이름 기반 파라미터 바인딩을 사용하려면 @Param 어노테이션을 사용하면 된다. 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하는 것이 좋다.

벌크성 수정 쿼리

  • 모든 데이터에 일괄적인 업데이트를 날려야 하는 경우 사용하는 것이 벌크성 수정 쿼리
  • @Modifying 어노테이션과 함께 사용
  • 벌크성 쿼리는 영속성 컨텍스트를 무시하기 때문에 영속성 컨텍스트에 있는 값과 DB에 있는 값이 달라질 수 있다. 따라서 영속성 컨텍스트에 엔티티가 없는 경우에 먼저 벌크성 수정 쿼리를 실행하거나, 영속성 컨텍스트에 엔티티가 있는 경우에 해야 한다면 벌크성 수정 쿼리 실행 후 영속성 컨텍스트를 초기화시킨다.
@Modifying(clearAutomatically = true)
@Query("update Product p set p.price = p.price * 1.1 where p.stockAmount < :stockAmount")
int bulkPriceUp(@Param("stockAmount") String stockAmount);

반환 타입

  • 스프링 데이터 JPA는 유연한 반환 타입을 지원하는데 결과가 한 건 이상이면 컬레션 인터페이스를 사용하고, 단건이면 반환 타입을 지정한다.
  • 만약 조회 결과가 없으면 컬렉션은 빈 컬렉션을 반환하고 단건은 null을 반환한다. 사실 단건으로 지정한 메소드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메소드를 호출하고, 조회 결과가 없으면 javax.persistence.NoResultException예외가 발생하는데 개발자 입장에서 다루기 불편하니 단건 조회시 이 예외가 발생하면 예외 무시하고 null을 반환.
  • 단건을 기대하고 반환 타입을 지정했는데 결과가 2건 이상 조회되면 javax.persistence.NonUniqueResultException 예외가 발생한다.

페이징과 정렬

쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 2가지 특별한 파라미터를 제공한다.

Sort

정렬기능

Pageable

  • 페이징 기능(내부에 sort 포함)
  • 파라미터에 Pageable을 사용하면 반환 타입으로 List나 Page를 사용할 수 있다. 
  • 반환 타입으로 Page를 사용하면 스프링 데이터 JPA는 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다
  • Pageable은 인터페이스라서 실제 사용할 때는 해당 인터페이스를 구현한 PageRequest 객체를 사용한다.
  • PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다.
public interface MemberRepository extends Repository<Member, Long>{
    // count 쿼리 사용
    Page<Member> findByName(String name, Pageable pageable);

    // count 쿼리 사용 안 함
    List<Member> findByName(String name, Pageable pageable);

    List<Member> findByName(String name, Sort sort);
}


PageRequest pageRequest = new PageRequest(0, 10, new Sort(Direction.DESC, "name"));

Page<Member> result = memberRepository.findByNameStartingWith("김", pageRequest);

List<MembeR> members = result.getContent(); // 조회된 데이터
int totalPages = result.getTotalPages(); // 전체 페이지 수
boolean hasNextPage = result.hasNextPage(); // 다음 페이지 존재 여부

3. 사용자 정의 리포지토리 구현

1) 개념

스프링 데이터 jpa로 리포지토리를 개발하면 인터페이스만 정의하고 구현체는 만들지 않는다. 하지만 다양한 이유로 메소드를 직접 구현해야 할 때도 있다. 그렇다고 리포지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해야 한다. 스프링 데이터 jpa는 이런 문제를 우회해서 필요한 메소드만 구현할 수 있는 방법을 제공한다.

2) 방법

public interface MemberRepositoryCustom{
	public List<Member> findMemberCustom();
}
// 클래스 이름을 짓는 규칙: 리포지토리 인터페이스 이름 + Impl
// 위 처럼 이름을 지어야 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다
public class MemberRepositoryImpl implements MemberRepositoryCustom{
	@Override
    public List<Member> findMemberCustom(){
    }
}
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom{
}

4. Web 확장

1) 개념

스프링 MVC에서 사용할 수 있는 편리한 기능

2) 방법

@EnableSpringDataWebSupport 어노테이션 사용

3) 종류

도메인 클래스 컨버터

  • http 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩
  • http요청으로 회원 아이디를 받지만 도메인 클래스 컨버터가 중간에 동작해서 아이디를 회원 엔티티 객체로 변환해서 넘겨준다.
  • 도메인 클래스 컨버터는 해당 엔티티와 관련된 리포지토리를 사용해서 엔티티를 찾는다
@Controller
public class MemberController{
	@RequestMapping("member/memberUpdateForm")
    public String memberUpdateForm(@RequestParam("id") Member member, Model model){
    	model.addAttribute("member", member);
        return "member/memberSaveForm";
    }
}

페이징, 정렬

  • 스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 mvc에서 편리하게 사용할 수 있도록 HandlerMethodArgumentResolver를 제공
  • 사용해야 할 페이징 정보가 둘 이상이면 접두사를 사용해서 구분, @Qualifier 어노테이션 사용
@RequestMapping(value = "/members", method = RequestMethod.GET)
public String list(Pageable pageable, Model model){
	Page<Member> page = memberService.findMembers(pageable);
    model.addAttribute("members", page.getContent());
    return "members/memberList";
}

// Pageable은 다음 요청 파라미터 정보로 만들어진다
// page: 현재 페이지, 0부터 시작
// size: 한 페이지에 노출할 데이터 건수
// sort: 정렬 조건을 정의한다.