JPQL은 JPA Query Language로 JPA에서 사용할 수 있는 쿼리를 의미한다. JQPL은 엔티티 객체를 대상으로 하기 때문에 매핑된 엔티티의 이름과 필드의 이름을 사용한다.
리포지토리에서 간단한 메서드는 제공하지만 좀 더 복잡하거나 내가 필요한 대로 조정하기 위해 쿼리 메서드를 사용한다.
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.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());
// @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);
@Query 어노테이션은 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있다. 이러한 점을 보완하기 위해 나온 것이 QueryDSL이다.
8.6.1 QueryDSL이란?
QueryDSL은 정적 타입(컴파일 시 타입에 대한 정보를 결정)을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 하는 프레임워크이다. QueryDSL이 제공하는 fluent api(코드를 읽기 쉽고 사용하기 편리하도록 설계된 api스타일 중 하나)를 활용해 쿼리를 생성할 수 있다.
8.6.2 QueryDSL의 장점
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() 메서드를 사용해야 하는데, 반환 메서드로 사용할 수 있는 메서드는 아래와 같다.
8.6.5 QuerydslPredicateExecutor, QuerydslRepositorySupport 활용
// 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기능은 사용할 수 없다는 단점이 있다.
//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 {
}
위와 같이 인터페이스를 구성하여 보통의 경우 서비스 레이어에서 리포지토리를 사용하는 것처럼 호출하여 사용 가능하다.
엔티티 클래스에는 '생성 일자', '변경 일자'와 같이 공통적으로 들어가는 필드가 있다. 엔티티 생성하거나 변경할 때 매번 이와 같은 필드들을 주입하는 번거로움이 있다. 이러한 번거로움을 해소하기 위해 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;
}
사용하는 Entity들은 위의 BaseEntity를 상속받으면, 공통된 필드가 자동으로 주입된다.