스프링 데이터 JPA

원종서·2022년 2월 12일
1

JPA

목록 보기
13/13

Spring data JPA

스프링이 제공하는 프로젝트이며 CRUD를 공통 인터페이스로 해결해준다.

구현체는 스프링 데이터 JPA가 생성해서 주입해줌으로 개발자가 직접 JPA 인터페이스를 구현하지 않아도 된다.

public interface MemberRepository extends JpaRepository<Member, Long> {}

스프링 데이터 프로젝트

JpaRepository 를 상속받고 사용할 수 있는 주요 메서드

T = 엔티티
ID = 엔티티의 식별자 타입
S = 엔티티와 그 자식

save(S) , 새로운 엔티티 저장하고 이미 있으면 수정
delete(T), 엔티티 하나 삭제
findOne(ID) 엔티티 하나 조회
getOne(Id) 엔티티를 프록시로 조회
findAll(...) 모든 엔티티 조회, 정렬이나 페이징 조건을 파라미터로 제공 가능 .

save()는 엔티티에 식별값이 없으면 새로운 엔티티로 판단해서 persist를 호출하고, 없다면 merge를 호출한다.

쿼리 메서드 기능

  1. 메서드 이름으로 쿼리 생성
  2. 메서드 이름으로 JPA NamedQuery 호출
  3. @Query 애노테이션을 사용해 리파지토리 인터페이스에 쿼리 직접 정의

1. 메서드 이름으로 쿼리 정의

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

findByEmailAndName메서드를 실행하면 스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL 을 생성하고 실행한다.

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

스프링 데이터 JPA 공식 문서가 제공하는 표의 규칙에 맞게 메소드 이름을 지으면 스프링 데이터 JPA가 알아서 JPQL 을 생성해준다.

키워드JQPL예
AndfindByLastnameAndFirstname...where x.lastname =?1 and x.firstname = ?2
OrfindByLastnameOrFirstname...where x.lastname =?1 or x.firstname = ?2
Is, EqualsfindByFirstname , findByFirstnamels, findByFirstnameEqauls...where x.firstname = ?1
BetweenfindByStartDateBetween...where x.startDate between 1? and 2?
LessThanfindByAgeLessThan...where x.age < ?1
GreaterThanfindByAgeGreaterThan... where x.age >1
LessThanEqualfindByAgeLEssThanEqual...where x.age <= ?1
AfterfindByStartDateAfter...where x.startDate > ?1
BeforefindByStartDateBefore...where x.startDate < ?1
IsNullfindByAgeIsNull...where x.age is null
IsNotNull, NotNullfindByAge(is)NotNull...where x.age is not null
LikefindByFirstnameLike...where x.firstname like = ?1
NotLikefindByFirstnameNotLike...where x.firstname like not = ?1
StartingWithfindByFirstnameStartingWith...where x.firstname like = ?1 (parameter bound with prepended %)
EndingWithfindByFirstnameEndingWith...where x.firstname like = ?1 (parameter bound with prepended %)
OrderByfindByAgeOrderByLastnameDesc...where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot...where x.lastname <> ?1
InfindByAgeIn(Collection age)...where x.age in ?1
NotInfindByAgeNotIn(Collection age)...where x.age not in ?1
TruefindByActiveTrue...where x.active = true
FalsefindByActiveFalse...where x.active = false

메서드 이름으로 JPA NamedQuery 호출

메소드 이름으로 JPA NamedQuery 를 호출하는 기능을 제공함.

// @NamedQuery 에노테이션으로 네임드 쿼리 정의

@Entity
@NamedQuery(
	name="Member.findByUsername"
	query="select m from Member m where m.username =  :username
)
public class Member {...}


// 스프링 데이터 JPA에서 네임드 쿼리 사용

public interface MemberRepository extends JpaRepository<Member, Long> { // 여기 선언한 Member 도메인 클래스

	List<Member> findByUsername(@Param("username") String username );
}

스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) . 메소드이름" 으로 NamedQuery를 찾아서 실행한다.

위의 예제는 Member.findByUsername

@Param는 이름 기반 파라미터를 바인딩할 때 사용하는 애노테이션

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

레포지토리에 메소드 직접 정의하려면 @Query 애노테이션 사용.
이름없는 네임드 쿼리라고도 불린다.


public interface MemberRepository extends JpaRepository<Member, Long> {
	@Query("select m from Member m where m.username = ?1")
    Member findByusername(String username);

// 네이티브 SQL 사용하려면  nativeQuery= true를 사용한다. 참고로 네이티브 SQL은 위치기반 파라미터가 0부터 시작한다.
	@Query(value ="SELECT * FROM MEMBER WHERE USERNAME = ?0 , nativeQuery= true) ;
	Member findByUsernameForNative(@Param("username") String username );
}

파라미터 바인딩

스프링 데이터 JPA는 위치기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원한다.
기본값은 위치기반 파라미터 바인딩이다
이름 기반 파라미터 반인딩을 하려면

매개변수에 @Param 애노테이션을 붙여준다.

@Query("select m from Member m where m.username = :username")
Member findByUsernameForNative(@Param("username") String username );

벌크성 수정 쿼리

@Modifying
@Query("update Product p set p.price =  p.price  * 1.1 where p.stockAmount < :stockAmount")
	int bulkPriceUp(@Param("stockAmount") String stockAmount);

벌크성 수정 ,삭제 쿼리는 @Modifying 애노테이션을 사용하면 된다.

벌크성 쿼리를 실행 후 영속성 컨텍스트를 초기화 하고 싶으면 @Modifying(clearAutomatically = true) 옵션을 지정해주면된다.

반환타입

스프링 데이터 JPA 는 유연한 반환타입 지원, 결과가 한 건 이상이면 컬렉션 인터페이스 사용하고, 단건이면 반한 타입을 지정.

만약 조히 결과가 없으면 컬렉션은 빈 컬렉션 반환, 단건은 null,
단 단건 조회 시 2개 이상 나오면 NonUniqueResultException 발생.

페이징과 정렬

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

Sort 형 파라미터 : 정렬기능
Pageable 형 파라미터 : 페이징기능( 내부 sort 포함)

Pageable 을 사용하면 반환타입으로 List or Page 타입을 받는다.
Page는 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다.

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

List<Member> findByName(String name , Pageable pageable); // count 쿼리 x

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

검색조건 : 이름이 김으로 시작하는 회원
정렬조건 : 이름 내림차순
페이징 조건 : 첫번째 페이지, 페이지당 10건

// 인터페이스 정의

public interface MemberRepository extends JpaRepository<Member , Long> {
Page findByNameStartWith(String name , Pageable pageable);
}

// 사용

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

Page result = memberRepository.findByNameStartWith("김" ,pageRequest);

List members = result.getContent(); // 조회된 데이터
int totalPage = result.getTotalPages(); // 전체 페이지 수
boolean hasNextPage = result.hasNextPage();

## 힌트

```java
@QueryHints(value ={ @QueryHint(name = "org.hibernate.readOnly", value="true")}, forCounting = true)
Page<Member> findByName(String name , Pageable pageable);

forCounting 속성은 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 카운트 쿼리에도 쿼리 힌트를 적용할지를
설정하는 옵션이다.

명세

책 도메인 주도 설계는 명세라는 개념을 소개한다.

스프링 데이터 JPA 는 JPA Criteria로 이 개념을 사용할 수 있도록 지원한다.

명세를 이해하기 위한 핵심단어는 술어(predicate)인데 이것은 단순히 참이나 거짓으로 평가된다.

이것은 ANO , OR 같은 연산자로 조합할 수 있다.

예로 데이터를 검색하기 위한 제약 조건 하나하나를 술어라 할 수 있다.

스프링 데이터 JPA는 술어를 Sepcification 클래스로 정의함.
Sepcification는 컴포지트 패턴으로 구성되어 여러 Sepcification을 조립할 수 있다.

따라서 다양한 검색조건을 조립해 새로운 검색조건을 만들 수 있다.

명세기능을 사용하라면 JpaSepcificationExcutor 인터페이스를 상속 받으면 된다.

public interface JpaSpecificationExecutor <T> {
	T findOne(Sepcification <T> spec);
    List<T> findAll(Sepcification<T> spec);
    Page<T> findAll(Sepcification<T> spec, Pageable pageable);
    List<T> findAll(Sepcification<T> spec, Sort sort);
    long count(Sepcification<T> secp);


public interface OrderRepository extends JpaRepository<Order,Long> , JpaSpecificationExecutor<Order> {
}

JpaSpecificationExecutor 의 메소드들은 Sepcification을 파라미터로 받아 검색조건으로 사용한다.

public List<Order> findOrders(String name) {
	List<Order> result = orderRepository.findAll(
    	where(memberName(name)).and(isOrderStatus())
   );
   
   return result;
)  

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

  1. 직접 구현할 메소드를 위한 사용자 정의 인터페이스 작성
public interface MemberRepositoryCustom {
	public List<Member> findMemberCustom();
}
  1. 사용자 정의 인터페이스를 구현한 클래스 작성
    규칙 ) 레포지토리이름 + Impl 로해야지 스프링 데이터 제이피에이가 사용자 정의 구현 클래스로 인식한다.
public class  MemberRepositoryCustomImpl implements MemberRepositoryCustom {
	public List<Member> findMemberCustom() {...}
}
  1. 마지막으로 스프링이 제공하는 인터페이스와 사용자 정의 인터페이스를 같이 구현하면 된다.
public interface MemberRepository extends JpaRepository<Member, Long> , MemberRepositoryCustom {
}

WEB 확장

편리한 기능 제공
1. 식별자로 도메인 클래스를 바로 바인딩해주는 도메인 클래스 컨버퍼,
2. 페이징과 정렬 기능

1. 도메인 클래스 컨버터 기능

HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아 바인딩 해준다.

ex /member/updade?id=1

// 도메인 클래스 ( 컨버터 기능 사용 x )
@Controller
public class MemberController {

@AutoWired
MemberRepository memberRepository;

@PostMapping("/member/updade")
public class memberUpdate(@RequestParam("id") Long id, Model model) {
	Member member = memberRepository.findOne(id);
		...
	}
}


// 도메인 클래스 ( 컨버터 기능 사용)
@Controller
public class MemberController {

@AutoWired
MemberRepository memberRepository;

@PostMapping("/member/updade")
public class memberUpdate(@RequestParam("id") Member member, Model model) {
    	soutv(member.getName());
	}
}

단 여기서 넘어온 엔티티를 수정해도 실제 데이터에서는 변경되지 않음

페이징과 정렬 기능

@RequestMapping(value ="/members")
public String list(Pageable pageable , Model model){
	Page<Member> page = memberService.findMember(pageable);

	model.addAttribute("members", page.getContent());

	return "member/memberList";
}

파라미터로 Pageable을 받은 것을 확인 할 수 잇다. Pageable은 다음 요청 파라미터 정보로 만들어진다.

page : 현재 페이지 0부터 시작
size ; 한 페이지에 노출할 데이터 수
sort: 정렬 조인을 정의한다

ex) /members?page=0&size=20&sort=name,desc&sort=address.city

접두사

사용할 페이지 정보가 둘 이상이라면 @Qualifier 애노ㅔ이션을 사용한다.

public String list(
	@Qualifier("member") Pageable memberPageable,
	@Qualifier("order") Pageable orderPageable), ...

{접두사명}_ 로 구분함
ex /members?member_page=0&order_page=1

기본값
page=0, size= 20

변경 시

@GetMapping("/members_page")
public String list(@PageableDefault(size=12, sort="name", direction=Sort.Direction.DESC) Pageable pageable){
	...
}

스프링 데이터 JPA 와 QueryDSL 통합

스프링 데이터 JPA는 2가지 방법로 QueryDSL를 지원한다.
1. QueryDslPredicateExecutor
2. QueryDslRepositorySupport

1. QueryDslPredicateExecutor 사용

public interface ItemRepository extends JpaRepository<Item, Long>, QueryDslPredicateExecutor<Item> {}

이제 상품 레파지로티에서 QueryDSL를 사용할 수 있다.

// QueryDSL 사용예제
QItem item = Qitem.item;

Iterable<Item> result = itemRepository.findAll(
	item.name.contains("장난감").and(item.price.between(1000,2000))
);

하지만 QueryDslPredicateExecutor는 스프링 데이터 jpa 에서 편리하게 QueryDSL 을 사영할 수 있지만 한계가 있다. join, fetch 를 상용할 수 없다.
따라서 JPAQuery를 직접 사용하거나 QueryDslRepositorySupport를 사용해야한다.

2. QueryDslRepositorySupport

QueryDSL의 모든 기능을 사용하려면 JPAQuery 객체를 직접 생성해서 사용하면 된다. 이때 QueryDslRepositorySupport를를 상속 받아 사용하면 편리하게 QueryDSL를 사용할 수 있다.


public interface CustomOrderRepository {
	public List<Order> search(OrderSearch orderSearch);
}
public class CustomOrderRepositoryImpl extends QueryDslRepositorySupport implements CustomOrderRepository {
	public CustomOrderRepositoryImpl (){
		super(Order.class);
	}
    
    @Override
    public List<Order> search(OrderSearch orderSearch) {
    	QOrder order = QOrder.order;
        QMember member =QMember.member;
        
        JPQLQuery query = from(order);
        
        if(StringUtils.hasText(orderSearch.getMemberName())){
        	query.leftJoin(order.member, member)
            	.where(member.name.contains(orderSearch.getMemberName()));
		}
        
        if(orderSearch.getOrderStatus() != null ) {
        	query.where(order.staus.eq(orderSearch.getOrderStatus()));
		}
        
        return query.lisr(order);
	}
}

0개의 댓글