자바 ORM 표준 JPA 프로그래밍 스터디 - 6주차 (Spring Data Jpa)

큰모래·2023년 6월 14일
0

스프링 데이터 JPA 소개


  • JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트
  • CRUD를 처리하기 위한 공통 인터페이스를 제공 (org.springframework.data.jpa.repository.JpaRepository)
  • 리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로
    생성해서 주입한다.
  • 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.
  • 일반적인 CRUD 뿐만 아니라 직접 작성한 MemberRepository.findByUsername() 같은 메서드도
    스프링 데이터 JPA가 분석해서 JPQL을 실행한다.

공통 인터페이스 기능


public interface MemberRepository extends JpaRepository<Member, Long> {
}
  • Repository, CrudRepository, PagingAndSortingRepository
    • 스프링 데이터 프로젝트가 공통으로 사용하는 인터페이스
  • JpaRepository
    • 스프링 데이터 JPA가 제공하는 JPA 특화된 기능을 사용하는 인터페이스

쿼리 메소드 기능


스프링 데이터 JPA 가 제공하는 쿼리 메소드 기능 3가지

  • 메소드 이름으로 쿼리 생성
  • 메소드 이름으로 JPA NamedQuery 생성
  • @Query 어노테이션을 이용해서 리포지토리 인터페이스에 쿼리 직접 정의

메소드 이름으로 쿼리 생성

//이메일과 이름으로 회원 조회
public interface MemberRepository extends Repository<Member, Long> {
		List<Member> findByEmailAndName(String email, String name);
}

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

JPA NamedQuery

  • 스프링 데이터 JPA는 메소드 이름으로 JPA Named Query를 호출하는 기능을 제공한다.
  • 도메인클래스 + . + 메소드 이름으로 스프링 데이터 JPA는 Named Query를 찾는다.
  • 없으면, 메소드 이름으로 쿼리 생성 전략을 사용한다.
//NamedQuery 정의
@Entity
@NamedQuery(
    name = "Member.findByUsername",
    query = "select m from Member m where m.username = :username")
public class Member {
    ...
}

//NamedQuery 호출
public interface MemberRepository extends JpaRepository<Member, Long> {

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

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

  • 리포지토리 메서드에 직접 쿼리를 정의한다.
  • 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 장점이 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
    
    @Query("select m from Member m where m.username = ?1")
    List<Member> findByUsername(String username);
}

파라미터 바인딩

  • 위치기반 바인딩, 이름 기반 바인딩 모두 지원한다.
  • 기본 값은 위치 기반이다.
  • 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하는게 좋다.
  • 이름기반 바인딩은 @Param으로 파라미터를 명시해준다.
select m from Member m where m.username = ?1 //위치기반
select m from Member m where m.username = :username  //이름기반

@Query("select m from Member m where m.username = :username")
List<Member> findByUsername(@Param("username") String username);

벌크성 수정 쿼리

  • 모든 데이터에 일괄적으로 업데이트를 날리는 경우
  • @Modifying 어노테이션을 사용한다. (꼭 넣어야 함!)
  • 벌크성 쿼리를 실행하고 영속성 컨텍스트를 초기화하고 싶으면 clearAutomatically 옵션을 true
    설정하면 된다.
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

반환 타입

  • 유연한 반환 타입을 지원한다.
  • 한건 이상일때는 컬렉션, 단건일때는 반환 타입 지정한다.
  • 조회 결과가 없으면 컬렉션은 빈 컬렉션, 단건은 null을 반환한다.
  • 단건으로 설정했는데 결과가 2개 이상이면 예외 발생

페이징과 정렬

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

  • Sort : 정렬 기능
  • Pageable : 페이징 기능(내부에 Sort 포함)
// count 쿼리 사용
Page<Member> findByName(String name, Pageable pageable);

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

List<Member> findByName(String name, Sort sort);
public interface MemberRepository extends Repository<Member, Long> {
		
		Page<Member> findByNameStartingWith(String name, Pageable pageable);
}

//실행코드
PageRequest pageRequest = new PageRequest(0, 10, new Sort(Direction.DESC, "name"));

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

힌트

  • JPA 구현체에게 제공하는 힌트
  • @QueryHint(name = "org.hibernate.readOnly", value = "true")
    • 조회하여 값을 변경할 수 없는 읽기 전용 쿼리
  • forCounting : 반환타입으로 Page 인터페이스를 적용하면 추가로 호출하는 count 쿼리에도 쿼리 힌트를 적용할지 설정하는 옵션

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

명세


  • 명세를 이해하기 위한 핵심 단어는 술어(Specification)
  • 술어는 데이터를 검색하기 위한 제약 조건 하나하나를 의미한다.
  • 이 술어를 조합해서 새로운 검색조건을 만들 수 있다.
  • Jpa Criteria 방식으로 명세를 사용할 수 있다.
// 인터페이스 정의
public interface OrderRepository extends JpaRepository<Order, Long>, 
		JpaSpecificationExecutor<Order> {

}

// 명세 정의
public class OrderSpec {

    public static Specification<Order> memberName(String memberName) {
        return new Specification<Order>() {
            public Predicate toPredicate(Root<Order> root,
                CriteriaQuery<?> query, CriteriaBuilder builder) {
                
                if (StringUtils.isEmpty(memberName)) return null;

                Join<Order, Member> m = root.join("member", JoinType.INNER);
                return builder.equal(m.get("name"), memberName);
            }
        }
    };

    // 위와 같은 방식으로 isOrderStatus() 구현
}

// 명세 사용
List<Order> result = orderRepository.findAll(
    where(memberName(name)).and(isOrderStatus())
);

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


  • 인터페이스만으로는 부족해서, 메서드를 직접 구현해야할 상황도 있다.
  • 이럴때는, 사용자 정의 인터페이스를 두고 해당 인터페이스를 구현하는 사용자 정의 클래스를 만들어서 사용
// 사용자 정의 인터페이스
public interface MemberRepositoryCustom {
    public List<Member> findMemberCustom();
}

// 사용자 정의 구현 클래스
public class MemberRepositoryImpl implements MemberRepositoryCustom {

    @Override
    public List<Member> findMemberCustom() {
        ...
    }
}

// 사용자 정의 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>, 
    MemberRepositoryCustom {

}

Web 확장


  • 스프링 데이터 프로젝트는 스프링 mvc에서 사용할 수 있는 편리한 기능을 제공한다.

설정

  • 설정을 완료하면 도메인 클래스 컨버터와 페이징과 정렬을 위한 HandlerMethodArgumentResolver
    빈으로 등록된다.
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
public class WebAppConfig {
}

도메인 클래스 컨버터 기능

  • RequestParam으로는 분명 id를 받고있는데 반환 객체는 Member이다.
  • 도메인 클래스 컨버터가 중간에 동작해서 아이디를 회원 엔티티 객체로 변환해서 넘겨준다.
  • 도메인 클래스 컨버터는 해당 엔티티와 관련된 리포지토리를 사용해서 엔티티를 찾는다.

//회원 조회 기능
@Controller
public class MemberController {

    @RequestMapping("member/memberUpdateForm")
    public String memberUpdateForm(@RequestParam("id") Member member, Model model) {
        model.addAttribute("member", member);
        return "member/memberSaveForm";
		}
}

페이징과 정렬 기능

  • 페이징과 정렬을 편리하게 사용하도록 HandlerMethodArgumentResolver 을 제공한다.
  • 페이징 기능 : PageableHandlerMethodArgumentResolver
  • 정렬 기능 : SortHandlerMethodArgumentResolver
  • 파라미터로 Pageable 객체를 받을 수 있다.
    • page
    • size
    • sort
@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";
}

스프링 데이터 JPA가 사용하는 구현체


  • 스프링 데이터 JPA가 제공하는 공통 인터페이스는 SimpleJpaRepository 클래스가 구현한다.
  • @Repository 적용: JPA 예외를 스프링이 추상화한 예외로 변환
  • @Transactional
  • save()
    • 저장할 엔티티가 새로운 엔티티면 저장하고, 이미 있는 엔티티면 병합(merge)한다.
    • 식별자가 객체일 때는 null
    • 기본 타입일 때 숫자 0 값이면 새로운 엔티티로 판단한다.
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, Id extends Serializable> implements
    JpaRepository<T, ID>, JpaSpecificationExecutor<T> {

    @Transactional
    public <S extends T> S save(S entity) {
        if (entityInformation.isnew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }
    ...
}

스프링 데이터 JPA와 QueryDSL 통합


스프링 데이터 JPA는 2가지 방법으로 QueryDSL을 지원한다.

  • org.springframework.data.querydsl.QueryDslPredicateExcutor
  • org.springframework.data.querydsl.QueryDslRepositorySupport

QueryDslPredicateExecutor

  • 편리하게 QueryDSL을 사용할 수 있지만 기능에 한계가 존재
  • join, fetch등을 사용할 수 없다 (JPQL에서 이야기하는 묵시적 조인은 사용가능)
  • 사용하려면 JPAQuery 혹은 QueryDslRepositorySupport 를 사용해야 한다.
public interface ItemRepository extends JpaRepository<Item, Long>, 
		QueryDslPredicateExecutor<Item> { }

QueryDslRepositorySupport

  • QueryDSL의 모든 기능을 사용하려면 JPAQuery 객체를 직접 생성해서 사용하면 된다.
  • 이때 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport을 상속 받아 사용하면 편리하게 가능
  • 아래 클래스를 구현클래스에서 상속 받아서 사용하면 된다.

@Repository
public abstract class QueryDslRepositorySupport {
		
		// 엔티티 매니저 반환
		protected EntityManager getEntityManager() {
				return entityManager;
		}

		// from 절 반환
		protected JPQLQuery from(EntityPath<?> ... paths) {
				return querydsl.createQuery(paths);
		}

		// QueryDSL delete 절 바환
		protected DeleteClause<JPADeleteClause> delete(EntityPath<?> path) {
				return new JPADeleteClause(entityManager, path);
		}

		// QueryDSL update 절 반환
		protected UpdateClause<JPAUpdateClause> update(EntityPath<?> path) {
				return new JPAUpdateClause(entityManager, path);
		}

		// 스프링 데이터 JPA가 제공하는 Querydsl을 편하게 사용하도록 돕는 헬퍼 객체 반환
		protected Querydsl getQuerydsl() {
				return this.querydsl;
		}
}
profile
큰모래

0개의 댓글