글은 스프링 부트 핵심 가이드(장정우 저)를 읽고 개인적으로 정리하기 위한 글입니다.
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 : 여러 조건을 묶을 때 사용
// 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);
- 쿼리 메서드에 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 어노테이션을 사용하는 메서드
@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 기능 활성화
- EnableJpaAuditing 추가
@SpringBootApplication
@EnableJpaAuditing
public class AdvancedJpaApplication {
public static void main(String[] args) {
SpringApplication.run(JpaApplication.class, args);
}
}
- Configuration 클래스 생성
@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를 스프링 빈으로 등록할 필요가 있다.
'북스터디 > 스프링 부트 핵심 가이드' 카테고리의 다른 글
Chapter 10. 유효성 검사와 예외처리 (0) | 2023.06.28 |
---|---|
Chapter 09. 연관관계 매핑 (0) | 2023.06.21 |
Chapter 06. 데이터베이스 연동 (3) | 2023.06.11 |
Chapter 05. API를 작성하는 다양한 방법 (0) | 2023.05.27 |
Chapter 04. 스프링 부트 애플리케이션 개발하기 (0) | 2023.05.27 |