글은 스프링 부트 핵심 가이드(장정우 저)를 읽고 개인적으로 정리하기 위한 글입니다.

 

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를 스프링 빈으로 등록할 필요가 있다.

+ Recent posts