이번에는 Spring Data JPA에서 제공하는 기능들에 대해서 알아보도록 할 것이다. 이전 글에서 공부했듯이 Spring Data JPA는 ORM 표준 기술 JPA를 쉽게 사용할 수 있도록 제공해주는 기술이다.
JPQL은 JPA에서 사용할 수 있는 쿼리를 의미한다. JPQL은 엔티티 객체를 대상으로 수행하는 쿼리이기 때문에 매핑된 엔티티으 이름과 필드의 이름을 사용한다.
예를 들어 Product 엔티티에서 JPQL을 사용하면 다음과 같다
SELET p FROM Product p WHERE p.number = ?1;
Product : dpsxlxl xkdlq
p.number : 엔티티 속성
레퍼지토리에서 JpaRepository를 상속받음으로서 다양한 CRUD 메서드를 제공받게 된다. 하지만 기본으로 제공되는 메서드들은 식별자 기반으로 생성되기 떄문에 별도의 메서드를 정의해서 사용하는 경우가 빈번히 발생한다고 한다.
이때 필요한게 쿼리 메서드이다.
AND, OR를 사용해 조건을 확장할수도 있다.
List<Person> findByLastnameAndEmail(String lastName, String email)
find : 주제
By : 서술어의 시작
LastnameAndEmail : 검색 및 정렬조건(여기서는 검색 조건)
And : 조건 확장을 위함
'...'으로 표시한 영역에는 도메인(엔티티)을 표현할 수 있다.
Opional<Product> findByNumber(Long number);
List<Product> findAllByName(String name);
Product queryByNumber(Long number);
boolean existByNumber(Long number);
long countByName(String name);
void deleteByNumber(Long number);
long removeByName(String name);
List<Product> findFirst5ByName(String name);
List<Product> findTop10ByName(Sring name)
서술어(Predicate) 부분에서 사용할 수 있는 조건자 키워드를 알아본다.
Product findByNumberIs(Long number);
Product findByNumberEquals(Long number);
Product findByNumberIsNot(Long number);
Product findByNubmerNot(Long number);
List<Proudct> findByUpdateAtNull();
List<Proudct> findByUpdateAtIsNull();
List<Proudct> findByUpdateAtNotNull();
List<Proudct> findByUpdateAtIsNotNull();
Product findByisActiceTrue();
Product findByisActiceIsTrue();
Product findByisActiceFalse();
Product findByisActiceIsFalse();
Product findByNumberAndName(Long number, String name);
Product findByNumberOrName(Long number, String name);
// Is는 모두 생략했다.
List<Product> findByPriceGreaterThan(Long price);
List<Product> findByPriceLessThan(Long price);
List<Product> findByPriceBetweenThan(Long lowerPrice, Long highPrice);
List<Product> findByNameLike(String name);
List<Product> findByNameContians(String name);
List<Product> findByNameStartingWith(String name);
List<Product> findByNameEndsWith(String name);
// Like를 사용할 경우
productRepostiroy.findByNameLike("%홍길동%");
// 이런식으로 %를 어느위치에 사용할지를 명시해야한다.
지금까지 설명한 쿼리 메스드를 사용해서 정렬과 페이징 처리를 할 수 있다.
SQL의 ORDER BY 구문을 동일하게 사용한다.
// 오름차순
List<Product> findByNameOrderByNumberAsc(String name);
// 내림차순
List<Product> findByNameOrderByNumberDesc(String name);
List<Product> findByNameOrderByPriceAscStockDesc(String name);
이렇게 메서드의 이름에 정렬 키워드를 삽입하여 사용하면 길이기 길어져서 가독성이 떨어질 수 있다.
이를 해결하기 위한 매개변수를 활용해 정렬을 하는 방법이 있다.
=> Sort 변수를 활용한다.
// 메소드
List<Product> findByName(String name, Sort sort);
// 사용방법
productRepository.findByName("펜", Sort.by(Order.asc("price"), Order.desc("stock")));
이렇게 매개변수를 사용하면 메소드이름에 키워드를 삽입했던것과 동일한 결과를 얻을 수 있다.
페이징이란?
데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것
웹 페이지에서 각 페이지를 구분해서 데이터를 제공할 때 그에 맞게 데이터를 용청하는 것이라고 생각하면 된다.
JPA 에서는 Page, Pageable을 사용하여 페징 처리를 한다.
// 메서드 작성
Page<Product> findByName(String name, Pageable pageable);
// 메서드 호출 방법
Pagea<Product> productPage = productRepository.findByName("펜", PageRequest.of(0,2));
// 출력
System.out.println(prdocutPage.getContent());
@Query("SELECT p FROM Prdocut p AS WHERE p.name=1?")
List<Product> findByName(String name);
@Query 어노테이션을 사용해 JPQL 형식의 쿼리문 작성
where절에 1은 처 번째 파라미터를 의미한다. 파라미터의 순서가 바뀌면 오류가 발생할 수 있으므로 @Param 어노테이션을 사용하는 것이 좋다. 그러면 코드의 가독성이 높아지고 유지보수가 수월해질수 있다.
@Query("SELECT p FROM Prdocut p AS WHERE p.name= :name")
List<Product> findByName(@Param("name") String name);
@Query("SELECT p.name, p.price, p.stock FROM Prdocut p AS WHERE p.name= :name")
List<Object[]> findByName(@Param("name") String name);
이때 리턴 타입은 Object배열의 리스트 형태로 리턴타입을 지정해야 한다.
메서드의 이름을 기반으로 생성하는 JPQL의 한계는 @Query 어노테이션을 통한 직접 작성으로 어느정도 해소할 수 있다. 하지만 이는 직접 문자열을 입력하는 것이기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있다. 실제 운영 환경에 애플리케이션을 배포하고 나서 오류가 발견되는 리스크를 유발한다.
이런 문제를 해결하기 위해 QueryDSL을 사용한다.
JPA에서 Audit는 '감시하다'라는 뜻이다.
각 데이터마다 '누가', '언제' 데이터를 생성했고 변경했는지 감시한다는 의미로 사용된다.
'생성일자', '변경 일자'같은 공통적인 필드를 반복해서 주입하는 번거로움을 없애고 자동으로 넣어주는 기능을 제공한다.
@EnableJpaAuditing 어노테이션을 추가하면 된다.
@SpringBootApplication
@EnableJpaAuditing
public class AdvancedJpaApplication{
public static void main(String[] args){
SpringApplication.run(JpaApplication.class, args);
}
}
위 코드처럼 어노테이션을 추가하면 정상작동 하지만 테스트 코드를 작성하고 애플리케이션을 테스트하는 일부 상황에서 오류가 발생할 수 있다.
따라서 별도의 Configuration 클래스를 생성해서 애플리케이션 클래스의 기능과 분리해서 활성화 하는것이 권장된다고 한다.
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration{
}
각 엔티티에 공통으로 들어가게 되는 컬름(필드)을 빼서 BaseEntity클래스를 만들어야 한다.
@Getter
@Setter
@ToString
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity{
@CreatedDate
@Column(updateable= false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
[ 어노테이션 설명 ]
@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualAndHashCode(callSuper = true)
@Table(name = "product")
public class Product extends BaseEntity{
@Id
@GeneratedValue(Strategy = GenerationType.IDENTITY)
private Lonag number;
@Column(nulable = false;
private String name;
@Column(nulable = false;
private Integer price;
@Column(nulable = false;
private Integer stock;
}
@ToString, @EqualsAndHashCode 어노테이션의 callSuper 속성은 부모 클래스의 필드를 포함하는 역할을 수행한다.
이렇게 되면 매번 LocalDateTime.now() 메서드를 사용해서 시간을 주입하지 않아도 자동으로 생성된다.
추가로 JPA Auditing 기능에 @CreatedBy, @ModifiedBy 어노테이션도 존재한다. 누가 엔티티를 생성했고 수정했느지 자동으로 값을 주입하는 어노테이션이다. 이기능을 사용하려면 AuditorAware를 스프링 빈으로 등록해야한다.
이번에는 JPQL을 메서드로 사용하는 방법과 @Query어노테이션을 사용하여 직접사용하는 방법을 알아보았다. 그리고 마지막에는 JPA Auditing을 사용하여 중복코드를 줄일 수 있는 방법도 알아보았다. 지금까지 진행한 프로젝트들에 JPQL을 사용해 왔었는데 이렇게 다양한 방법이 있는지 몰랐다. 평소에는 findById(), findBNumber() 등의 정말 기본적인 것들을 조합해서 사용해 왔다. 쿼리 메서드를 생성하는 규칙을 알게되어서 좀 더 다양한 메서드를 만들수 있을것 같고 지금까지 기본적인 메서드를 조합했던 것들을 하나의 메서드로 변경할 수 있어 데이터베이스의 부담을 줄일 수 있을 것일고 생각했다.
또한 마지막에 JPA Auditing은 너무 필요했던 기술이었다. 매번 Entity를 작성할 때 마다 "코딩은 반복이 일상인가보다..." 하는 생각을 해왔는데 이제 어느정도의 중복 코드는 대체할 수 있게 되었다. 많은사람들이 사용했겠지만 나는 이번에 처음 알게 되었고 빨리 적용을 해보고 싶다는 생각이든다.
북스터디 절반정도 진행했는데 크게 느낀점이 있다. 지금까지 검색으로만 공부를 해오다 보니 구현방법은 알아도 원리나 조금 더 이론적인 부분은 알기 힘들고 항상 찝찝함이 남았다. 그런데 책을 읽어보니 중간중간 비어있던 공간을 채울 수 있었고 이해가 가지않던 동작방법도 채워줄 수 있었다. 이 맛에 개발공부하는가보다 싶다.