JVM/JPA

스프링 데이터 JPA

kyoulho 2023. 7. 2. 12:48

소개

단순 CRUD를 처리하기 위하여 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다.

스프링 데이터 JPA는 스프링 데이터 프로젝트의 하위 프로젝트로 JPA에 특화된 기능을 제공한다.

 

스캔 범위 설정

@EnableJpaRepositories 어노테이션으로 범위를 지정해 줄 수 있으며 없을 시에는 @SpringBootApplication에서 설정한 빈 스캔 범위에서 스캔한다.

@Configuration
@EnableJpaRepositories(basePackages = "jpaboo.jpashop.repository")
public class AppConfig {}

 

JPARepository 주요 메소드

메소드 기능
save(S) 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다.
엔티티에 식별자 값이 없으면 em.persist() 호출
식별자 값이 있으면 em.merge() 호출
delete(T) 엔티티 하나를 삭제한다. 내부에서 em.remove()를 호출
findOne(ID) 엔티티 하나를 조회한다. 내부에서 em.find()를 호출
getOne(ID) 엔티티를 프록시로 조회한다. 내부에서 em.gereference()를 호출
findAll(...) 모든 엔티티를 조회한다. 정렬이나 페이징 조건을 파라미터로 제공할 수 있다.

 

쿼리 메소드 기능

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

스프링 JPA가 제공하는 쿼리 메소드 기능은 크게 3가지가 있다.

1. 메소드 이름으로 쿼리 생성

키워드 JPQL 예
And findByLastnameAndFirstname ... WHERE x.lastname = ?1 AND x.firstname = ?2
Or findByLastnameOrFirstname ... WHERE x.lastname = ?1 OR x.firstname = ?2
Is,
Equals
findByFirstname,
findByFirstnameIs,
findByFirstnameEquals
... WHERE x.firstname =1?
Between findByStartDateBetween ... WHERE x.startDate BETWEEN ?1 AND ?2
LessThan findByAgeLessThan ... WHERE x.age < ?1
LessThanEqual findByAgeLessThanEqual ... WHERE x. age <= ?1
GreaterThan findByAgeGreaterThan ... WHERE x. age > ?1
GreaterThanEqual findByAgeGreaterThanEqual ... WHERE x.age >= ?1
After findByStartDateAfter ... WHERE x.startDate > ?1
Before findByStartDateBefore ... WHERE x.startDate < ?1
IsNull findByAgeIsNull ... WHERE x.age IS NULL
IsNotNull
NotNull
findByAge(Is)NotNull ... WHERE x.age NOT NULL
Like findByFirstnameLike ... WHERE x.firstname LIKE ?1
NotLike findByFirstnameNotLike ... WHERE x.firstname NOT LIKE ?!
StartingWith findByFirstnameStartingWith ... WHERE x.firstname LIKE %?1
EndingWith findByFirstnameEndingWith ... WHERE x.firstname LIKE ?1%
Containing findByFirstnameContaining ... WHERE x.firstname LIKE %?1%
OrderBy findByAgeOrderByLastnameDesc ... WHERE x.age = ?1 ORDER BY x.lastname DESC
Not findByLastnameNot ... WHERE x.lastname <> ?1
In findByAgeIn(Collection ages) ... WHERE x.age IN ?1
NotIn findByAgeNotIn(Collection ages) ... WHERE x.age NOT IN ?1
TRUE findByActiveTrue() ... WHERE x.active = true
FALSE findByActiveFalse() ... WHERE x.active = false
IgnoreCase findByFirstnameIgnoreCase ... WHERE UPPER(x.firstname) = UPPER(?1)

 

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);
}

 

3. @Query, 리포지토리 메소드에 쿼리 정의

이 방법은 실행할 메소드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다. 또한 JPA Named 쿼리처럼 애플리케이션 실행 시저에 문법 오류를 발견할 수 있는 장점이 있다.

public interface MemberRepository extends JpaRepository<Member, Long> {
	
    // JPQL은 위치 기반 파라미터를 1부터 시작
    @Query("SELECT m FROM Member m WHERE m.username = ?1")
    Member findByUsername(String username)
    
    // SQL은 위치 기반 파라미터를 0부터 시작
    @Query(value = "SELECT * FROM MEMBER WHERE USERNAME = ?0")
    Member findByUsername(String username);
    
    // 이름 기반 파라미터 바인딩
    @Query("SELECT m FROM Member m WHERE m.username = :name")
    Member findByUsername(@Param("name")String username);
}

 

벌크성 수정 쿼리

스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 org.springframework.data.jpa.repository.Modifying 어노테이션을 사용하면된다.

벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶으면 @Modifying(clearAutomatically = true)를 사용한다. 기본값은 false 이다.

@Modifying
@Query("UPDATE Product p SET p.price = p.price * 1.1 WHERE p.stockAmout < :stockAmout")
int buildPriceUp(@param("stockAmount") String stockAmount);

 

반환 타입

결과가 한 건 이상이면 컬렉션 인터페이스를 사용하고, 단건이면 반환 타입을 지정한다.

결과가 없으면 컬렉션은 빈 컬렉션을 반환하고, 단건은 null을 반환한다.

단건 반환타입을 지정했는데 결과가 2건 이상 조회되면 javax.persistence.NonUniqueResultException 예외가 발생한다.

 

 

페이징과 정렬

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

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

// 정렬
List<Member> findByName(String name, Sort sort);

// 첫 번째 페이지, 페이지당 보여줄 데이터는 10건, 이름으로 내림차순
PageRequest pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "name"));
memberRepository.findByNameStartingWith("김", pageRequest);

List<Member> content = result.getContent();
int totalPages = result.getTotalPages();
boolean hasNextPage = result.hasNext();

 

힌트

JPA 쿼리 힌트를 사용하려면 org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용하면 된다.

이것은 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트다.

@QueryHints(value = {@QueryHint(name = "org.hibernate.readOnly", value = "true")},
			forCounting = true) // count 쿼리에도 쿼리 힌트를 적용할지를 설정하는 옵션
Page<Member> findByName(String name, Pageable pageable);

 

Lock

쿼리시 락을 걸려면 org.springframework.data.jpa.repository.Lock 어노테이션을 사용하면 된다.

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByName(String name);

 

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

사용자 정의 구현 클래스 이름 끝에 Impl 대신 다른 이름을 붙이고 싶으면 @EnableJpaRepositories.repositoryImplementationPostfix 설정에 사용할 postfix를 넣으면 된다.

// 인터페이스 작성
public interface MemberRepositoryCustom {
    public List<Member> findMemberCustom();
}

// 구현
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom{
    @Override
    public List<Member> findMemberCustom() {...}
}

// 상속
public interface MemberRepository
		extends JpaRepository<Member, Long>, MemberRepositoryCustom {}
728x90

'JVM > JPA' 카테고리의 다른 글

컬렉션  (0) 2023.07.02
스프링 데이터 JPA와 QueryDSL 통합  (0) 2023.07.02
객체지향 쿼리 심화  (0) 2023.06.30
스토어드 프로시저(JPA 2.1)  (0) 2023.06.30
네이티브 SQL  (0) 2023.06.30