이번 스터디 정리글에서는 Spring Data JPA에 대해서 다뤄 보려고 한다.
이전 6장에서 학습한 코드를 가져와서 사용할 예정이며 JPA의 기능에 대해 살펴 보기 위해 spring initializer 등을 통해 프로젝트를 생성해 준다.
JPQL은 JPA Query Language로 JPA에서 사용하는 쿼리이다.
JPQL는 SQL과 상당히 유사하지만 SQL과 다른 점은 테이블과 칼럼을 사용하는 대신 엔티티 객체에 대한 이름과 필드의 이름을 사용한다는 점이다.
<JPQL 쿼리 기본구조>
SELECT p FROM Product p WHERE p.number = ?1;
리포지토리는 기본적으로 JpaRepository를 상속 받음으로서 CRUD 메서드를 사용할 수 있다. (*Create, Read, Update, Delete) 하지만 기본 메서드는 식별자를 기반으로 생성되기 때문에 별도의 메서드를 정의해서 사용하는 경우가 많아진다. 이때 쿼리문 작성을 위해 사용하는 것이 쿼리 메서드이다.
쿼리 메서드의 구성 요소로는 크게 주제와 서술어가 있다.
< 리포지토리 쿼리 메서드 생성 예시>
(리턴 타입) + { 주제 + 서술어 (속성) }
List<Person> findByLastnameAndEmail(String lastName, String email);
<쿼리 메서드 주제의 주요 키워드>
...으로 표시한 부분에는 엔티티를 사용할 수 있으나, 리포지토리에서 먼저 도메인을 선언하고 메서드를 이용하기 때문에 생략하기도 한다.
▷ find ... By
조회하는 기능을 수행하는 키워드.
예시) Optional<Product> findByNumber(Long number)
▷ exits ... By
특정 데이터가 존재하는지 확인하는 키워드. 반환 값으로는 boolean을 사용한다.
예시) boolean existsByNumber(Long number);
▷ count ... By
조회 쿼리를 수행한 후 쿼리 결과로 나온 레코드 개수를 리턴하는 키워드.
예시) long countByName(String name);
▷ delete ... By, remove ... By
삭제 쿼리를 수행하는 키워드. 리턴 타입이 없거나 삭제 횟수를 사용한다.
예시) void deleteByNumber(Long number);
▷ ... first<number> ..., ... Top<number> ...
쿼리를 통해 조회한 결과값의 개수를 제한하는 키워드. 주제와 By 사이에 넣어서 사용한다.
주로 한번의 동작으로 여러번의 조회할 때 사용한다. 단일 조회에 대해서는 <number>를 생략한다.
예시) List<Product> findFirst5ByName(String name);
조건자 키워드는 쿼리 메서드의 서술어 부분에서 사용하는 키워드이다.
▷ Is
값의 일치를 조건으로 사용하는 조건자 키워드. Equals와 동일한 기능을 수행하며 생략 가능 하다.
예시) Product findByNumberIs(Long number);
▷ (Is) Not
값의 불일치를 조건으로 사용하는 조건자 키워드.
예시) Product findByNumberIsNot(Long number);
▷ (Is)Null, (Is)NotNull
값이 Null인지 검사하는 조건자 키워드.
예시) List<Product> findUpdatedAtNull();
▷ True, (Is)False
boolean 타입으로 지정된 칼럼의 값을 확인하는 조건자 키워드.
예시) Product findByisActiveTrue();
▷ And, Or
여러 조건을 묶을 때 사용하는 조건자 키워드.
예시) Product findByNumberAndName(Long number, String name);
▷ GreaterThan, (Is)LessThan, (Is)Between
숫자, datetime 칼럼에 대한 비교 연산이 가능한 조건자 키워드.
예시) List<Product> findByPriceIsGreaterThan(Long price);
▷ (Is)StartingWith(==StartsWith), (Is)EndingWith(==EndsWith), (Is)Containing(==Contains), (Is)Like
예시) List<Product> findByNameLike(String name);
일반적인 쿼리문에서 정렬을 처리할 때는 Order By 구문을 이용한다.
▶ 정렬 조건이 1개
예시) List<Product> findByNameOrderByNumberAsc(String name);
위의 메서드를 통해서는 들어온 name을 바탕으로 데이터베이스에서 조회하고 조회된 칼럼에 대해 number의 값으로 다시 정렬하여 전달한다.
만약 정렬 조건을 여러개로 하고 싶다면 별다른 조건 키워드 없이 정렬 조건을 나열하면 된다.
▶ 정렬 조건이 2개 이상
예시) List<Product> findByNameOrderByPriceAscStockDesc(String name);
위의 메서드는 표기한 순서대로 price에 대해서 먼저 정렬을 하고 그 다음 Stock에 대해서 정렬을 수행한다.
메서드의 이름에 정렬 키워드를 넣지 않고 정렬을 하는 방법도 있다. 가독성을 위해 쿼리 메서드의 이름에 넣는 것 대신 매개변수를 이용하여 정렬을 수행한다.
▶ 정렬 키워드 분리
예시) List<Product> findByName(Stirng name, Sort sort);
호출 예시) product.Repository.findByName("펜", Sort.by(Order.asc("price")));
메서드에서 정렬을 넣는 것 대신 매개변수를 활용해도 역시 호출에서의 가독성이 좋지 않다. 그럴 때는 sort 부분을 하나의 메서드로 분리한뒤 쿼리 메서드를 호출하는 방법도 사용할 수 있다.
페이징은 데이터베이스 레코드를 개수로 나누어서 페이지를 구분하는 것을 의미한다.
JPA는 Page와 Pageable을 통해서 페이징 처리를 한다.
예시) Page<Product> findByName(Sring name, Pageable pageable);
호출 예시) Page<Product> productPage = productRepository.findByName("펜", "PageRequest.of(0, 2));
PageRequst는 Pageable의 구현체이다. PageRequest는 of 메서드를 통해 PageRequest 객체를 생성한다.
< of 메서드>
page 객체를 출력하면 객체의 값이 아니라 객체의 페이지 위치 정보만 전달 하기 때문에 값을 보기를 원한다면 아래의 예시처럼 작성한다.
예시) Page<Product> productPage = productRepository.findByName("펜", pageRequest.of(0, 2);
System.out.println(productPage.getContent());
.getContent() 메서드를 통해 배열 형태로 값을 출력할 수 있다.
데이터베이스에서 값을 조회할때 직접 쿼리 메서드를 작성해도 되지만, @Query 어노테이션으로도 직접 JPQL을 작성할 수도 있다. 직접 JPQL을 사용하면 데이터베이스에 특화된 SQL의 작성이 가능하다. 튜닝된 쿼리를 이용하고자 할때 SQL을 직접 작성한다.
@Query("SELECT p FROM Product AS p WHERE p.name = ?1")
List<Product> findByName(String name);
@Query ( ) 안에 JPQL 형식의 쿼리문을 작성한다.
- FROM 문에는 엔티티 타입을 명시하고 별칭을 생성한다.
- WHERE 문에는 조건을 명시한다.
- ?1의 경우 파라미터를 전달 받기 위한 인자이다. 1은 첫 번째 파라미터를 의미한다.
?1 대신 파라미터를 정확하게 전달하기 위해서는 @Param 어노테이션을 사용하는 것이 좋다.
예시)
@Query("SELECT p FROM Product AS p WHERE p.name = :name")
List<Product> findByNameParam(@Param("name") String name);
@Query를 통해서 엔티티가 아닌 원하는 칼럼의 값만 추출하는 것도 가능하다.
이럴 때는 반환 값의 타입에 Object 형태의 리스트 타입으로 해야 한다.
예시)
@Query("SELECT p.name, p.price, p.stock FROM Product p WHERE p.name = :name")
List<Object[]> findByNameParam2(@Param("name") String name);
@Query 어노테이션을 이용하여 직접 JPQL을 작성할 수 있지만, 이 경우 문자열을 입력하기 때문에 컴파일 과정에서 에러를 잡지 못하고 런타임시 에러가 발생할 수 있다. 이런 문제를 해결하기 위한 것이 QueryDSL이다.
QueryDSL을 통해 문자열 대신 코드로 쿼리를 작성할 수 있다.
QueryDSL은 정적 타입을 이용해 SQL 같은 쿼리를 생성할 수 있도록 지원하는 프레임 워크이다.
문자열이나 XML 파일을 이용한 쿼리 생성 대신 QueryDSL이 제공하는 플루언트 API를 사용하여 쿼리를 생성한다.
http://querydsl.com/static/querydsl/4.0.1/reference/ko-KR/html_single/
QueryDSL 사용을 위해서는 설정이 필요하다. pom.xml에 다음과 같은 내용을 추가한다.
설정을 마친 다음에는 compile 버튼을 통해 빌드 작업을 수행한다.
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
(pom.xml)
<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>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
<options>
<querydsl.entityAccessors>true</querydsl.entityAccessors>
</options>
</configuration>
</execution>
</executions>
</plugin>
(pom.xml)
▶ JPAQuery 객체 사용
@PersistenceContext
EntityManager entityManager;
@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("----------------");
}
}
위에서는 QueryDSL의 Q도에인 클래스를 사용하고 있다.
QueryDSL을 사용하기 위해서는 JPAQuery 객체를 사용해야 한다. JPAQuery 객체는 entityManager를 통해서 생성한다.
그리고는 builder형식으로 쿼리를 작성한다. 빌더 메서드는 SQL에서 사용하는 키워드로 구성한다.
List 타입으로 값을 받기 위해서는 fetch() 메서드를 사용해야 한다.
<반환 메서드>
▶ JPAQueryFactory 사용
@Test
void queryDslTest2() {
//JPAQueryFactory 초기화
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
List<Product> productList = jpaQueryFactory.selectFrom(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
// 원하는 칼럼만
List<String> productList = jpaQueryFactory
.select(qProduct.name)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
// 조회 대상이 여러 개인 경우
List<Tuple> tupleList = jpaQueryFactory
.select(qProduct.name, qProduct.price)
.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("----------------");
}
}
JPAQuery와의 차이점은 select부터 작성이 가능하다.
전체 칼럼이 아니라 원하는 칼럼만을 조회하고 싶다면 selectFrom() 대신 select()와 from()을 구분하여 사용한다.
조회 대상이 여러 개일 경우에는 리턴 타입을 List<Tuple>로 지정해준다.
config로 JPAQueryFactory를 @Bean에 등록하면 JPAQueryFactory를 초기화 하는 과정 없이 사용이 가능하다.
@Configuration
public class QueryDSLConfiguration {
@PersistenceContext
EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
▶ QuerydslPredicateExecutor 인터페이스
JpaRepository와 함께 리포지토리에서 QueryDSL 사용할 수 있는 인터페이스를 제공한다.
public interface QProductRepository extends JpaRepository<Product, Long>,
QuerydslPredicateExecutor<Product> {
}
위처럼 두가지를 동시에 상속 받아서 사용한다.
<QuerydslPredicateExecutor의 제공 메서드>
QuerydslPredicateExecutor 인터페이스의 메서드는 predicate 타입을 매개변수로 받는다.
predicate는 표현식을 작성할수 있도록 QueryDSL에서 제공하는 인터페이스이다.
@Test
public void queryDSLTest() {
//predicate 사용
Predicate predicate = QProduct.product.name.containsIgnoreCase("펜")
.and(QProduct.product.price.between(1000, 2500));
Optional<Product> foundProduct = qProductRepository.findOne(predicate);
// predicate 서술부만 사용
QProduct qProduct = QProduct.product;
Iterable<Product> productList = qProductRepository.findAll(
qProduct.name.contains("팬")
.and(qProduct.price.between(550, 1500))
);
if(foundProduct.isPresent()){
Product product = foundProduct.get();
System.out.println(product.getNumber());
System.out.println(product.getName());
System.out.println(product.getPrice());
System.out.println(product.getStock());
}
}
위처럼 predicate를 사용해서 표현식을 정의해도 되고 서술부만 가져다 사용해도 된다.
▶ QuerydslRepositorySupport
추상 클래스 사용cutomerRepository를 사용해서 리포지토리를 구현하는 방식
package com.springboot.advanced_jpa.data.repository.support;
import com.springboot.advanced_jpa.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository("ProductRepositorySupport")
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom{
}
( ProductRepoitory.java)
위와 같이 상속을 받는다.
public interface ProductRepositoryCustom {
List<Product> findByName(String name);
}
( ProductRepoitoryCustom.java)
구현할 메서드를 정의해 놓는다.
@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;
}
}
( ProductRepoitoryCustomImpl.java)
정의해 놓은 메서드를 구현한다.
JPA Auditing은 데이터 생성 및 변경 날짜를 관리해준다.
@EnableJpaAuditing 어노테이션을 사용해서 spring boot 애플리케이션의 Auditing 기능을 활성화 한다.
@EnableJpaAuditing
@SpringBootApplication
public class AdvancedJpaApplication {
public static void main(String[] args) {
SpringApplication.run(AdvancedJpaApplication.class, args);
}
}
위의 어노테이션 설정 대신 @WebMvcTest 어노테이션의 호출 예외 발생 등을 처리하기 위해 별도의 configuration 파일을 아래와 같이 만들어서 Auditing 기능을 활성화 해주어도 된다.
@Configuration
@EnableJpaAuditing
public class JoaAuditingConfiguration {
}
코드의 중복을 없애기 위해 각 엔티티에 공통으로 들어가는 칼럼은 하나의 클래스로 빼두어야 한다.
생성일자와 변경일자만 아래와 같이 빼둔다.
@Getter
@Setter
@ToString
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@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;
}
기존 엔티티에 부모 엔티티로 baseEntity를 상속해주고 @EqualsAndHashCode(callSuper = true) 어노테이션을 통해서 부모 클래스 필드를 포함하도록 한다.
작성자: 니나노
[스프링 1팀] 10-11장. 유효성 검사와 예외처리 및 액츄에이터 (0) | 2023.12.22 |
---|---|
[스프링 1팀] 9장 연관관계 매핑 (1) | 2023.12.20 |
[스프링1] 7장. 테스트 코드 작성하기 (0) | 2023.11.17 |
[스프링1] 6. 데이터베이스 연동 (0) | 2023.11.10 |
[스프링 1팀] 5-6장. API를 작성하는 다양한 방법 및 데이터베이스 연동 (0) | 2023.11.03 |