글은 스프링 부트 핵심 가이드(장정우 저) 를 읽고 개인적으로 정리하기 위한 글입니다.
JPQL
JPA Query Language, JPA에서 사용할 수 있는 쿼리를 의미
SQL과 매우 비슷해서 DB 쿼리에 익숙한 사람이라면 어렵지 않게 사용 가능
-- JPQL 쿼리의 기본 구조
SELECT p FROM Product p WHERE p.number = ?1;
쿼리 메서드 살펴보기
리포지토리는 JpaRepository를 상속받는 것만으로도 다양한 CRUD 메서드를 제공
하지만 이러한 기본 메서드들은 식별자 기반으로 생성되기 때문에 결국 별도의 메서드를 정의해서 사용하는 경우가 많다.
간단한 쿼리문을 작성하기 위해 사용되는 것이 쿼리 메서드
쿼리 메서드의 생성
쿼리 메서드는 크게 동작을 결정하는 주제(Subject)와 서술어(Predicate)로 구분
// 리턴타입 + (주제+서술어(속성)) 구조의 메서드
List<Person> findByLastnameAndEmail(String lastName, String email)
쿼리 메서드의 주제 키워드
find…By
read…By
get…By
query…By
search…By
stream…By
조회하는 기능을 수행하는 키워드 … 으로 표시한 영역에는 도메인(엔티티)을 표현할 수 있다.
그러나 리포지토리에서 이미 도메인을 설정한 후에 메서드를 사용하기 때문에 중복으로 판단해 생략하기도 한다.
쿼리 메서드의 조건자 키워드
Is : 값의 일치를 조건으로 사용하는 조건자 키워드. 생략되는 경우가 많으며 Eqauls와 동일한 기능을 수행
(Is)Not : 값의 불일치를 조건으로 사용하는 조건자 키워드. Is는 생략하고 Not 키워드만 사용할 수도 있다.
// Not 키워드를 사용한 쿼리 메서드
Product findByNumberIsNot(Long number);
Product findByNumberNot(Long number);
(Is)Null, (Is)NotNull : 값이 null인지 검사하는 조건자 키워드
// Null 키워드를 사용한 쿼리 메서드
List<Product> findByUpdatedAtNull();
List<Product> findByUpdatedAtIsNull();
List<Product> findByUpdatedAtNotNull();
List<Product> findByUpdatedAtIsNotNull();
(Is)True, (Is)False : boolean 타입으로 지정된 칼럼값을 확인하는 키워드
// True, False 키워드를 사용한 쿼리 메서드
Product findByisActiveTrue();
Product findByisActiveIsTrue();
Product findByisActiveFalse();
Product findByisActiveIsFalse();
// And, Or 키워드를 사용한 쿼리 메서드
Product findByNumberAndName(Long number, String name);
Product findByNumberOrName(Long number, String name);
(Is)Greater Than, (Is)LessThan, (Is)Between : 숫자나 datetime 칼럼을 대상으로 한 비교 연산에 사용할 수 있는 조건자 키워드. Greater Than, LessThan, 키워드는 비교 대상에 대한 초과/ 미만의 개념으로 비교 연산을 수행, 경곗값을 포함하려면 Equal 키워드를 추가하면 된다.
// GreaterThan LessThan, Between 키워드를 사용한 쿼리 메서드
List<Product> findByPriceIsGreaterThan(Long price);
List<Product> findByPriceGreaterThan(Long price);
List<Product> findByPriceGreaterThanEqual(Long price);
List<Product> findByPriceIsLessThan(Long price);
List<Product> findBypriceLessThan(Long price);
List<Product> findBypriceLessThanEqual(Long price);
List<Product> findByPriceIsBetween(Long lowPrice, Long highPrice);
List<Product> findByPriceBetween(Long lowPrice, Long highPrice);
Is)StartingWith(==StartsWith), (Is)EndingWith(==EndsWith), (Is)Containing(==Contains), (Is)Like : 컬럼 값에서 일부 일치 여부를 확인하는 조건자 키워드. SQL 쿼리문에서 값의 일부를 포함하는 값을 추출할 떄 사용하는 ‘%’ 키워드와 동일한 역할을 하는 키워드. 자동으로 생성되는 SQL문을 보면 Containing 키워드는 문자열의 양 끝, StartingWith 키워드는 문자열의 앞, EndingWith 키워드는 문자열의 끝에 ‘%’가 배치
// 부분 일치 키워드를 사용한 쿼리 메서드
List<Produuct> findByNameLike(String name);
List<Product> findByNameIsLike(String name);
List<Product> findByNameContains(String name);
List<Product> findByNameIsContaining(String name);
List<Product> findByNameStartsWith(String name);
List<Product> findByNameStartingWith(String name);
List<Product> findByNameIsStartingWith(String name);
List<Product> findByNameEndsWith(String name);
List<Product> findByNameEndingWith(String name);
List<Product> findByNameIsEndingWith(String name);
정렬과 페이징 처리
정렬 처리하기
// Asc : 오름차순, Desc : 내림차순
List<Product> findByNameOrderByNumberAsc(String name);
List<Product> findByNameOrderByNumberDesc(String name);
// And를 붙이지 않음
List<Product> findByNameOrderByPriceAscStockDesc(String name);
List<Product> findByName(String name, Sort sort);
productRepository.findByName("펜", Sort.by(Order.asc("price")));
productRepository.findByName("펜", Sort.by(Order.asc("rpcie"), Order.desc("stock")));
@SpringBootTest
class ProductRepositoryTest {
@Autowired
ProductRepository productRepository;
@Test
void springAndPagingTest() {
.. 상단 코드 생략..
Sytem.out.println(productRepository.findByName("펜", getSort()));
}
private Sort getSort() {
return Sort.by(
Order.asc("price"),
Order.desc("stock")
);
}
}
페이징 처리
페이징이란 데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것을 의미
JPA에서는 이 같은 페이징 처리를 위해 Page와 Pageable을 사용
Page<Product> findByName(String namme, Pageable pageable);
Page<Product> productPage = productRepository.findByName("펜", PageReuqest.of(0, 2));
@Query 어노테이션 사용하기
@Query 어노테이션을 사용해 직접 JPQL을 작성할 수도 있다.
JPQL을 사용하면 JPA 구현체에서 자동으로 쿼리 문장을 해석하고 실행하게 된다.
보통 튜닝된 쿼리를 사용하고자 할 때 직접 SQL을 작성
@Query("SELECT p From Product p Where p.name = ?1")
List<Product> findByName(String name);
@Query 어노테이션과 @Param 어노테이션을 사용하는 메서드
@Query("SELECT p fROM pRODUCT P wHERE P.Name = :name")
List<Product> findByNameParma(@Param("name") String name);
@Query("SELECT p.name, p.price. p.stock From Product p WHERE p.name = :name")
List<Object[]> findByNameParam2(@Param("name")String name);
QueryDSL?
정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크
문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 플루언트(Fluent) API를 활용해 쿼리를 생성할 수 있음
QueryDSL의 장점
IDE가 제공하는 코드 자동 완성 기능 사용할 수 있다.
문법적으로 잘못된 쿼리를 허용하지 않는다. 따라서 정상적으로 활용된 QueryDSL은 문법 오류를 발생시키지 않는다.
고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있다.
코드로 작성하므로 가독성 및 생산성이 향상
도메인 타입과 프로퍼티를 안전하게 참조할 수 있다.
QueryDSL 관련 의존성 추가
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
QueryDSL에서 JPAAnnotationProcessor 사용을 위한 APT 플러그인 추가
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>src/main/generated</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
기본적인 QueryDSL 사용하기
List<T> fetch() : 조회 결과를 리스트로 반환
T fetchOne : 단 한건의 결과를 반환
T fetchFirst() : 여러 건의 조회 결과 중 1건을 반환. 내부 로직을 살펴보면 .limit(1).fetchOne() 으로 구현돼 있다.
Long fetchCount() : 조회 결과의 개수를 반환
QueryResult<T> fetchResults() : 조회 결과 리스트와 개수를 포함한 QueryResults를 반환합니다.
QuerydslPredicateExecutor, QuerydslRepositorySupport 활용
스프링 데이터 JPA에서는 QueryDSL을 더욱 편하게 사용할 수 있게 QuerydslPredicateExecutor 인터페이스와 QuerydslRepositorySupport 클래스를 제공한다.
QuerydslPredicateExecutor 인터페이스
QuerydslPredicateExecutor 는 JpaRepository와 함께 리포지토리에서 QueryDSL을 사용할 수 있게 인터페이스를 제공
package com.springboot.advanced_jpa.data.repository;
import com.springboot.advanced_jpa.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
public interface QProductRepository extends JpaRepository<Product, Long>,
QuerydslPredicateExecutor<Product> {
}
// Reference (https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/querydsl/QuerydslPredicateExecutor.html)
QuerydslPredicateExecutor에서 제공하는 메서드
Optional<T> findOne(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
Iterable<T> findAll(Predicate predicate, Sort sort);
Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>...orders);
Iterable<T> findAll(OrderSpecifier<?>...orders);
Page<T> findAll(Predicate pridicate, Pageable pageable);
long count(Predicate predicate);
boolean exists(Predicate predicate);
QuerydslRepositorySupport 추상 클래스 사용하기
QuerydslRepositorySupport 클래스 역시 QueryDSL라이브러리를 사용하는데 유용한 기능을 제공
가장 보편적으로 사용하는 방식은 customRepository를 활용해 리포지토리를 구현하는 방식
JpaRepository와 QuerydslRepositorySupport는 Spring Data JPA에서 제공하는 인터페이스와 클래스, 나머지 ProductRepository와 ProductRepositoryCustom, ProductRepositoryCustomImpl은 직접 구현해야 한다.
JpaRepository를 상속받는 ProductRepository를 생성
이때 직접 구현한 쿼리를 생성하기 위해서는 JpaRepository를 상속받지 않는 리포지토리 인터페이스인 ProductRepositoryCustom을 생성. 이 인터페이스에 정의하고자 하는 기능들을 메서드로 정의
ProductRepositoryCustom에서 정의한 메서드를 사용하기 위해 ProductRepository에서 ProductRepositoryCustom을 상속받습니다.
ProductRepositoryCustom에서 정의된 메서드를 기반으로 실제 쿼리를 작성하기 위해 구현체인 ProductRepositoryCustomImpl 클래스를 생성
ProductRepositoryCustomImpl 클래스에서는 다양한 방법으로 쿼리를 구현할 수 있지만 QueryDSL을 사용하기 위해 QueryDslRepositorySupport를 상속받는다.
package com.springboot.advanced_jpa.data.repository.support;
import com.springboot.advanced_jpa.data.entity.Product;
import java.util.List;
/**
* 필요한 쿼리를 작성할 메소드를 정의하는 인터페이스
*/
public interface ProductRepositoryCustom {
List<Product> findByName(String name);
}
package com.springboot.advanced_jpa.data.repository.support;
import com.springboot.advanced_jpa.data.entity.Product;
import com.springboot.advanced_jpa.data.entity.QProduct;
import java.util.List;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Component;
@Component
public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport implements
ProductRepositoryCustom {
public ProductRepositoryCustomImpl() {
super(Product.class);
}
@Override
public List<Product> findByName(String name) {
QProduct product = QProduct.product;
List<Product> productList = from(product)
.where(product.name.eq(name))
.select(product)
.fetch();
return productList;
}
}
// Reference (https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/support/QuerydslRepositorySupport.html)
// JPAQuery를 활용한 QueryDSL 테스트 코드
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.springboot.advanced_jpa.data.entity.Product;
import com.springboot.advanced_jpa.data.entity.QProduct;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.time.LocalDateTime;
import java.util.List;
@SpringBootTest
class ProductRepositoryTest {
@Autowired
ProductRepository productRepository;
// 이후 코드는 QueryDSL 관련 테스트입니다.
@PersistenceContext
EntityManager entityManager;
// 예제 8.26
@Test
void queryDslTest() {
JPAQuery<Product> query = new JPAQuery(entityManager);
QProduct qProduct = QProduct.product;
List<Product> productList = query
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (Product product : productList) {
System.out.println("----------------");
System.out.println();
System.out.println("Product Number : " + product.getNumber());
System.out.println("Product Name : " + product.getName());
System.out.println("Product Price : " + product.getPrice());
System.out.println("Product Stock : " + product.getStock());
System.out.println();
System.out.println("----------------");
}
}
// 예제 8.27
@Test
void queryDslTest2() {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
List<Product> productList = jpaQueryFactory.selectFrom(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (Product product : productList) {
System.out.println("----------------");
System.out.println();
System.out.println("Product Number : " + product.getNumber());
System.out.println("Product Name : " + product.getName());
System.out.println("Product Price : " + product.getPrice());
System.out.println("Product Stock : " + product.getStock());
System.out.println();
System.out.println("----------------");
}
}
// 예제 8.28
@Test
void queryDslTest3() {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
List<String> productList = jpaQueryFactory
.select(qProduct.name)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (String product : productList) {
System.out.println("----------------");
System.out.println("Product Name : " + product);
System.out.println("----------------");
}
List<Tuple> tupleList = jpaQueryFactory
.select(qProduct.name, qProduct.price)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (Tuple product : tupleList) {
System.out.println("----------------");
System.out.println("Product Name : " + product.get(qProduct.name));
System.out.println("Product Name : " + product.get(qProduct.price));
System.out.println("----------------");
}
}
@Test
void queryDslTupleTest() {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
List<Tuple> productList = jpaQueryFactory
.select(qProduct.name, qProduct.price)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (Tuple product : productList) {
System.out.println("----------------");
System.out.println("Product Name : " + product.get(qProduct.name));
System.out.println("Product Price : " + product.get(qProduct.price));
System.out.println("----------------");
}
}
// 예제 8.30
/**
* Bean 객체로 등록된 JPAQueryFactory를 활용
*/
@Autowired
JPAQueryFactory jpaQueryFactory;
@Test
void queryDslTest4() {
QProduct qProduct = QProduct.product;
List<String> productList = jpaQueryFactory
.select(qProduct.name)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (String product : productList) {
System.out.println("----------------");
System.out.println("Product Name : " + product);
System.out.println("----------------");
}
}
}
JPA Auditing 적용
Audit : 감시하다
각 데이터마다 ‘누가’, ‘언제’ 데이터를 생성했고 변경했는지 감시한다는 의미로 사용
대표적으로 많이 사용되는 필드
이러한 필드들은 매번 엔티티를 생성하거나 변경할 때마다 값을 주입해야 하는 번거로움이 있음
Spring Data JPA에서는 이러한 값을 자동으로 넣어주는 기능을 제공
JPA Auditing 기능 활성화
@SpringBootApplication
@EnableJpaAuditing
public class AdvancedJpaApplication {
public static void main(String[] args) {
SpringApplication.run(JpaApplication.class, args);
}
}
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {
}
BaseEntity 만들기
코드의 중복을 없애기 위해서는 각 엔티티에 공통으로 들어가게 되는 칼럼(필드)을 하나의 클래스로 빼는 작업을 수행해야 한다.
package com.springboot.advanced_jpa.data.entity;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@Getter
@Setter
@ToString
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
@MappedSuperClass : JPA 엔티티 클래스가 상속받을 경우 자식 클래스에게 매핑 정보를 전달
@EntiryListeners : 엔티티를 데이터베이스에 적용하기 전후로 콜백을 요청할 수 있게 하는 어노테이션
AuditingEntityListener : 엔티티의 Auditing 정보를 주입하는 JPA 엔티티 리스너 클래스입니다.
@CreateDate : 데이터의 생성 날짜를 자동으로 주입하는 어노테이션
@LastModifiedDate : 데이터 수정 날짜를 자동으로 주입하는 어노테이션
// BaseEntity를 상속받은 Product 엔티티 클래스
package com.springboot.advanced_jpa.data.entity;
import lombok.*;
import javax.persistence.*;
@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Table(name = "product")
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long number;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer price;
@Column(nullable = false)
private Integer stock;
}
JPA Auditing 기능에는 @CreatedBy, @ModifiedBy 어노테이션도 존재. 누가 엔티티를 생성했고 수정했는지 자동으로 값을 주입하는 기능 이 기능을 사용하려면 AuditorAware를 스프링 빈으로 등록할 필요가 있다.