[스프링부트핵심가이드] 08. Spring Data JPA 활용

오늘내일·2023년 11월 12일
0

책 리뷰

목록 보기
6/11

8.2 JPQL

JPQL은 JPA Query Language로 JPA에서 사용할 수 있는 쿼리를 의미한다. JQPL은 엔티티 객체를 대상으로 하기 때문에 매핑된 엔티티의 이름과 필드의 이름을 사용한다.

8.3 쿼리 메서드 살펴보기

리포지토리에서 간단한 메서드는 제공하지만 좀 더 복잡하거나 내가 필요한 대로 조정하기 위해 쿼리 메서드를 사용한다.

8.3.1 쿼리 메서드의 생성
쿼리메서드는 리포지토리 인터페이스에서 아래와 같이 생성한다.

List<Member> findByLastnameAndEmail(String lastName, String email);

8.3.2 쿼리 메서드의 주제 키워드

// find...By
Optional<Member> findById(Long id);
List<Member> findAllById(Long id);

// exists...By
boolean existsById(Long id);

// count...By
long countById(Long id);

// delete...By, remove...By
void deleteById(Long id);
long removeById(Long id);

// ...First<number>..., ...Top<number>...
List<Member> findFirst5ById(Long id);
List<Member> findTop10ById(Long id);

8.3.3 쿼리 메서드의 조건자 키워드

// Not
Member findByIdNot(Long id);

// Null, Not Null
List<Member> findByUpdatedAtNull();
List<Member> findByUpdatedAtNotNull();

// True, False
Member findByActiveTrue();
Member findByActiveFalse();

// And, Or
Member findByIdAndName(Long id, String name);
Member findByIdOrName(Long id, String name);

// GreaterThan, LessThan, Between
List<Member> findByAgeGreaterThan(Integer age);
List<Member> findByAgeGreaterThanEqual(Integer age);

List<Member> findByAgeLessThan(Integer age);
List<Member> findByAgeLessThanEqual(Integer age);

List<Member> findByAgeBetween(Integer lowAge, Integer highAge);

// StartsWith, EndsWith, Contains, Like
List<Member> findByNameStartsWith(String name);
List<Member> findByNameEndsWith(String name);
List<Member> findByNameContains(String name);
List<Member> findByNameLike(String name);

// Like 호출
results = memberRepository.findByNameLike("%bert%");

8.4 정렬과 페이징 처리

8.4.1 정렬 처리하기

// OrderBy...Asc, OrderBy...Desc
List<Member> findByNameOrderByAgeAsc(String name);
List<Member> findByNameOrderByAgeDesc(String name);

// 쿼리 메서드에 여러 정렬 기준 사용
List<Member> findByNameOrderByAgeDescNumberAsc(String name);

// 매개변수를 활용한 쿼리 정렬
List<Member> findByName(String name, Sort sort);

// 쿼리 메서드에 Sort 객체 전달
memberRepository.findByName("원빈", Sort.by(Order.asc("age"));
memberRepository.findByName("원빈", Sort.by(Order.desc("age"),Order.asc("number")));

// Sort 부분을 아래와 같이 메서드로 분리해서 쿼리 메서드 호출 가능
private Sort getSort() {
	return Sort.by(
    		Order.desc("age"),
            Order.asc("number")
    );
}

8.4.2 페이징 처리

// 페이징 처리를 위한 쿼리 메서드 예시
Page<Member> findByName(String name, Pageable pageable);

// 페이징 쿼리 메서드 호출하는 방법
// 매개변수 : (페이지 번호, 페이지당 데이터 개수)
Page<Member> memberPage = memberRepository.findByName("원빈", PageRequest.of(0, 2));

// 페이지 객체 내용 보려면 getContent() 사용
Page<Member> memberPage = memberRepository.findByName("원빈", PageRequest.of(0, 2));
System.out.println(memberPage.getContent());

8.5 @Query 어노테이션 사용하기

// @Query 어노테이션을 사용하는 메서드
@Query("SELECT m FROM Member AS m WHERE m.name = ?1")
List<Member> findByName(String name);

@Query어노테이션의 쿼리문에서 'AS'는 생략 가능하다. 쿼리문에서 '?1'은 첫번째 파라미터를 전달받는다는 의미이다. 이와 같이 쿼리문에서 파라미터를 순서로 표기하여 전달받으면 오류 가능성이 커지기 때문에 @Param을 사용하는 것이 좋다.

// @Query 어노테이션과 @Param 어노테이션을 사용한 메서드
@Query("SELECT m FROM Member m WHERE m.name = :name")
List<Member> findByNameParam(@Param("name") String name);
// 특정 칼럼만 추출하는 쿼리(리턴 타입을 Object배열을 담는 List로 해야함)
@Query("SELECT m.name, m.age, m.email FROM Member m WHERE m.name = :name")
List<Object[]> findByNameParam2(@Param("name") String name);

8.6 QueryDSL 적용하기

@Query 어노테이션은 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있다. 이러한 점을 보완하기 위해 나온 것이 QueryDSL이다.

8.6.1 QueryDSL이란?
QueryDSL은 정적 타입(컴파일 시 타입에 대한 정보를 결정)을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 하는 프레임워크이다. QueryDSL이 제공하는 fluent api(코드를 읽기 쉽고 사용하기 편리하도록 설계된 api스타일 중 하나)를 활용해 쿼리를 생성할 수 있다.

8.6.2 QueryDSL의 장점

  • IDE의 suggestion을 사용할 수 있다.
  • 문법적으로 잘못된 쿼리를 허용하지 않는다.
  • 고정된 SQL쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있다.
  • 코드로 작성하므로 가독성 및 생상성이 향상된다.
  • 도메인 타입과 프로퍼티를 안전하게 참조할 수 있다.

8.6.3 QueryDSL을 사용하기 위한 프로젝트 설정
gradle 기준 build.gradle 파일의 아래와 같이 디펜던시를 추가한다.

// queryDSL
    implementation 'com.querydsl:querydsl-jpa:5.0.0'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'
// NoClassDefFoundError 방지
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

QueryDSL은 지금까지 작성했던 엔티티 클래스와 Q도메인(Qdomain)이라는 쿼리 타입의 클래스를 자체적으로 생성해서 메타데이터로 사용하며, 이를 통해 SQL과 같은 쿼리를 생성해서 제공한다.

QueryDSL 설정 관련 참고 사이트 :
https://lordofkangs.tistory.com/461
https://mag1c.tistory.com/446

8.6.4 기본적인 QueryDSL 사용하기
QueryDSL을 활용하는 방법으로는 JPAQuery 객체를 사용하는 방법과 JPAQueryFactory 객체를 사용하는 방법이 있다. 두 방법 모두 사용하는 방식이 비슷하므로 JPAQueryFactory를 사용하는 방법을 알아보자.

QueryDSL을 비즈니스 로직에 활용할 수 있게 설정부터 해야 한다. QueryDSL을 사용하기 전 아래와 같은 컨피그 파일부터 생성한다.

@Configuration
public class QueryDslConfig {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory(){
        return new JPAQueryFactory(entityManager);
    }
}

JPAQueryFactory 객체를 @Bean 객체로 등록해두면 매번 JPAQueryFactory를 초기화하지 않고 스프링 컨테이너에서 가져다 쓸 수 있다.

// JPAQueryFactory 빈을 활용한 테스트 코드
@Autowired
JPAQueryFactory queryFactory;

@Test
void queryDslTest{
	QMember qMember = QMember.product;
    
    List<String> memberList = queryFactory
    	.select(qMember.name)
        .from(qMember)
        .where(qMember.name.eq("원빈"))
        .orderBy(qMember.age.desc())
        .fetch();
    
    for (String member : memberList){
    	System.out.println("Member name : " + member);
    }
}

리스트 타입으로 리턴받기 위해서는 fetch() 메서드를 사용해야 하는데, 반환 메서드로 사용할 수 있는 메서드는 아래와 같다.

  • List< T > fetch() : 조회 결과를 리스트로 반환한다.
  • T fetchOne() : 단 건의 조회 결과를 반환한다.
  • T fetchFirst() : 여러 건의 조회 결과 중 1건을 반환한다.
  • Long fetchCount() : 조회 결과의 개수를 반환한다.
  • QueryResult< T > fetchResults() : 조회 결과 리스트와 개수를 포함한 QueryResults를 반환한다.

8.6.5 QuerydslPredicateExecutor, QuerydslRepositorySupport 활용

  • QueryPredicateExecutor 인터페이스
// QueryPredicateExecutor를 사용하는 리포지토리 생성
public interface memberRepository extends JpaRepository<Member, Long>, 
				QueryPredicateExecutor<Member>{

}

// Predicate를 활용한 findOne() 메서드 호출
@Autowired
MemberRepository memberRepository;

@Test
void queryDslTest{
	Predicate predicate = QMember.member.name.containsIgnoreCase("원빈")
    	.and(QMember.member.age.between(20, 30));
        
    Optional<Member> foundMember = memberRepository.findOne(predicate);
    
    if (foundMember.isPresent()) {
    	Member member = foundMember.get();
        System.out.println(member.getName());
    }
}

QueryPredicateExecutor 인터페이스를 활용하면 더욱 편하게 QueryDSL을 사용할 수 있지만 join이나 fetch기능은 사용할 수 없다는 단점이 있다.

  • QuerydslRepositorySupport 추상 클래스 사용하기
    QuerydslRepositorySupport 추상 클래스를 사용하는 보편적인 방식은 CustomRepository를 활용해 리포지토리를 구현하는 방식이다. 아래와 같은 구조로 활용한다.
    1) 앞에서 사용했던 방식처럼 JpaRepository를 상속받는 MemberRepository를 생성한다.
    2) 직접 구현한 쿼리를 사용하기 위해서 JpaRepository를 상속받지 않는 리포지토리 인터페이스인 MemberRepositoryCustom을 생성한다. 이 인터페이스에 정의하고자 하는 기능들을 메서드로 정의한다.
    3) MemberRepositoryCustom에서 정의한 메서드를 사용하기 위해 MemberRepository에서 MemberRepositoryCustom을 상속받는다.
    4) MemberRepository에서 정의된 메서드를 기반으로 실제 쿼리 작성을 하기 위해 구현체인 MemberRepositoryCustomImpl 클래스를 생성한다.
    5) MemberRepositoryCustomImpl클래스에서 QueryDSL을 사용하기 위해 QuerydlsRepositorySupport를 상속받는다.
//MemberRepositoryCustom 인터페이스
public interface MemberRepositoryCustom {
	List<Member> findByName(String name);
}

// MemberRepositoryCustomImpl 클래스
@Component
public class MemberRepositoryCustomImpl extends QuerydslRepositorySupport 
	implements MemberRepositoryCustom{
    //  생성자를 통해 도메인 클래스를 부모 클래스에 전달해야 함.
    MemberRepositoryCustom {
   		public MemberRepositoryCustomImpl() {
        	super(Member.class)
        }
    }
    
    @Override
    public List<Member> findByName(String name){
    	QMember member = QMember.member;
        
        List<Member> memberList = from(member)
        	.where(member.name.eq(name))
            .select(member)
            .fetch();
            
        return memberList;
    }
}

QuerydlsRepositorySupport가 제공하는 기능 중 대표적인 기능이 from() 메서드이다. from() 메서드는 어떤 도메인에 접근할 것인지 지정하는 역할을 수행하고 JPAQuery를 리턴한다.

// MemberRepository 인터페이스
@Repository
public interface MemberRepository extends JpaRepository<Member, Long>, 
	MemberRepositoryCustom {
    
}

위와 같이 인터페이스를 구성하여 보통의 경우 서비스 레이어에서 리포지토리를 사용하는 것처럼 호출하여 사용 가능하다.

8.7 JPA Auditing 적용

엔티티 클래스에는 '생성 일자', '변경 일자'와 같이 공통적으로 들어가는 필드가 있다. 엔티티 생성하거나 변경할 때 매번 이와 같은 필드들을 주입하는 번거로움이 있다. 이러한 번거로움을 해소하기 위해 JPA Auditing 기능을 사용한다.

8.7.1 JPA Auditing 기능 활성화
JPA Auditing 기능 활성화를 위해 main() 메서드가 있는 클래스에 @EnableJpaAuditing 어노테이션을 추가하면 된다. 하지만 이와 같은 방식은 애플리케이션을 테스트하는 일부 상황에서 오류가 발생할 수 있다. 따라서 아래와 같이 애플리케이션 클래스와 분리를 위해 Configuration 클래스를 별도 생성하는 방법이 권장된다.

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {

}

8.7.2 BaseEntity 만들기
매번 사용되는 생성일자, 변경일자와 같은 필드들을 BaseEntity클래스로 생성한다.

@Getter
@Setter
@ToString
@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class})
public class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime modifiedAt;
}
  • @MappedSuperclass : JPA의 엔티티 클래스가 상속받을 경우 자식 클래스에 매핑 정보를 전달한다.
  • @EntityLiseners : 엔티티를 데이터베이스에 적용하기 전후로 콜백*을 요청할 수 있게 하는 어노테이션이다.
    (콜백 함수 : 1) 다른 함수의 파라미터로써 이용되는 함수, 2) 어떤 이벤트에 의해 호출되어지는 함수)
  • AuditingEntityListener : 엔티티의 Auditing 정보를 주입하는 JPA 엔티티 리스터 클래스이다.
  • @CreatedDate : 데이터 생성 날짜를 자동으로 주입하는 어노테이션이다.
  • @LastModifiedDate : 데이터 수정 날짜를 자동으로 주입하는 어노테이션이다.

사용하는 Entity들은 위의 BaseEntity를 상속받으면, 공통된 필드가 자동으로 주입된다.

profile
다시 시작합니다.

0개의 댓글