8. Spring Data JPA 활용
- Spring Data JPA의 자세한 내용 아래 링크에서 확인 가능하다.
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
Spring Data JPA - Reference Documentation
Example 121. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del
docs.spring.io
8.1 프로젝트 생성
8.2 JPQL
JPQL은 JPA Query Language의 줄임말로 JPA에서 사용가능한 쿼리를 의미한다. JPQL의 문법은 SQL과 매우 비슷하여 데이터 베이스 쿼리에 익숙하다면 어렵지 않게 사용 가능하다.
SQL과 JPQL의 차이점은 SQL에서는 테이블이나 칼럼의 이름을 사용한다. 반면에 JPQL은 엔티티 객체를 대상으로 수행하는 쿼리를 날리기 때문에 매핑된 엔티티의 이름과 필드의 이름을 사용한다.
8.3 쿼리 메서드 살펴보기
- 리포지토리는 JpaRepository를 상속받는 것만으로도 다양한 CRUD 메서드를 제공한다.
- 하지만 이러한 기본 메서드들은 식별자 기반으로 생성되기 때문에 결국 별도의 메서드를 정의해서 사용하는 경우가 많다.
- 이때 간단한 쿼리문을 작성하기 위해 사용되는 것이 쿼리 메서드이다.
8.3.1 쿼리 메서드의 생성
- 쿼리 메서드는 크게 동작을 결정하는 주제(Subject)와 서술어(Predicate)로 구분한다.
- ‘find…By’, ‘exists…By’, ‘get…By’와 같은 키워드로 쿼리의 주제를 정하며 ‘By’는 서술어의 시작을 나타내는 구분자 역할을 한다.
- 서술어 부분은 검색 및 정렬 조건을 지정하는 영역이다.
- 기본적으로 엔티티의 속성을 정의가 가능하고, AND나 OR를 사용해 조건을 확장하는 것도 가능하다.
* 리포지토리의 쿼리 메서드 생성 예시
// (리턴타입) + {주제 + 서술어(속성)} 구조의 메서드
List<Person> findByLastnameAndEmail(String lastName, String email);
8.3.2 쿼리 메서드의 주제 키워드
- find...By
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
- read...By
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> readByUsername(String username);
}
- get...By
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> getByFirstNameAndLastName(String firstName, String lastName);
}
- query...By
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.age = :age")
List<User> queryByAge(@Param("age") int age);
}
- search...By
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
List<User> searchByCity(String city);
}
- stream...By
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.city = :city")
Stream<User> streamByCity(@Param("city") String city);
}
- exists...By
특정 데이터가 존재하는지 확인하는 키워드
boolean existsByNumber(Long number);
- count...By
조회 쿼리를 수행한 후 쿼리 결과로 나온 레코드의 개수를 리턴
long countByName(String name);
- delete...By, remove...By
삭제 쿼리를 수행, 리턴 타입이 없거나 삭제한 횟수를 리턴
void deleteByNumber(Long number);
long removeByName(String name);
- ...First<number>..., ...Top<number>...
쿼리를 통해 조회된 결괏값의 개수를 제한하는 키워드
List<Product> findFirst5ByName(String name);
List<Product> findTop10ByName(String name);
8.3.3 쿼리 메서드의 조건자 키워드
- Is
- 값의 일치를 조건으로 사용하는 조건자 키워드이다.
- 생략되는 경우가 많으며 Equals와 동일한 기능을 수행한다.
// findByNumber 메서드와 동일하게 동작
Product findByNumberIs(Long number);
Product findByNumberEquals(Long number);
- (Is)Not
- 값의 불일치를 조건으로 사용하는 조건자 키워드이다.
- Is는 생략하고 Not 키워드만 사용할 수도 있다.
Product findByNumberIsNot(Long number);
Product findByNumberNot(Long number);
- (Is)Null, (Is)NotNull
- 값이 null 인지 검사하는 조건자 키워드이다.
List<Product> findByUpdateAtNull();
List<Product> findByUpdateAtIsNull();
List<Product> findByUpdateAtNotNull();
List<Product> findByUpdateIsNotNull();
- (Is)True, (Is)False
- boolean 타입으로 지정된 칼럼 값을 확인하는 키워드이다.
Product findByisActiveTrue();
Product findByisActiveIsTrue();
Product findByisActiveFalse();
Product findByisActiveIsFalse();
- And, Or
- 여러 조건을 묶을 때 사용한다.
Product findByNumberAndName(Long number, String name);
Product findByNumberOrName(Long number, String name);
- (Is)GreaterThen, (Is)LessThan, (Is)Between
- 숫자나 datetime 칼럼을 대상으로 한 비교 연산에 사용할 수 있는 조건자 키워드이다.
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 쿼리문에서 값의 일부를 포함하는 값을 추출할 때 사용하는 '%' 키워드와 동일한 역할을 한다.
List<Product> findByNameLike(String name);
List<Product> findByNameIsLike(String name);
List<Product> findByNameContains(String name);
List<Product> findByNameContaining(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);
8.4 정렬과 페이징 처리
8.4.1 정렬 처리하기
- 일반적인 쿼리문에서 정렬을 사용할 때는 ORDER BY 구문을 사용
// Asc: 오름차순, Desc: 내림차순
List<Product> findByNameOrderByNumberAsc(String name);
List<Product> findByNameOrderByNumberDesc(String name);
// And를 붙이지 않음
List<Product> findByNameOrderByPriceAscNumberDesc(String name);
// 매개변수를 활용한 쿼리 정렬
List<Product> findByName(String name, Sort sort);
// 쿼리 메서드에 Sort 객체 전달
productRepository.findByName("펜", Sort.by(Order.asc("price")));
productRepository.findByName("펜", Sort.by(Order.asc("price"), Order.desc("stock")));
8.4.2 페이징 처리
- 페이징이란 데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것을 의미한다.
- JPA에서는 이 같은 페이징 처리를 위해 Page와 Pageable을 사용한다.
- 아래의 쿼리 메서드를 해석하면 ‘상품정보를 이름으로 검색한 후 상품 번호로 오름차순 정렬을 수행’한다는 뜻이다.
// 페이징 처리를 위한 쿼리 메서드 예시
Page<Product> findByName(String name, Pageable pageable);
// 페이징 쿼리 메서드를 호출하는 방법
Page<Product> productPage = productRepository.findByName("펜", PageRequest.of(0, 2));
// Page 객체의 데이터 출력
Page<Product> productPage = productRepository.findByName("펜", PageRequest.of(0, 2));
System.out.println(productPage.getContent());
- 페이징 쿼리 메서드를 호출하는 방법 중
of 메서드 | 매개변수 설정 | 비고 |
of(int page, int size) | 페이지 번호(0부터 시작), 페이지당 데이터 개수 | 데이터를 정렬하지 않음 |
of(int page, int size, Sort) | 페이지 번호, 페이지당 데이터 개수, 정렬 | sort에 의해 정렬 |
of(int page, int size, Direction, String... properties) | 페이지 번호, 페이지당 데이터 개수, 정렬 방향, 속성(칼럼) | Sort.by(direction, properties)에 의해 정렬 |
8.5 @Query 어노테이션 사용하기
- 값을 가져올 때, @Query 어노테이션을 사용해 직접 JPQL을 작성할 수 있다.
- JPQL을 사용하면 JPA 구현체에서 자동으로 쿼리 문장을 해석, 실행한다.
// @Querty 어노테이션을 사용하는 메서드
@Query("SELECT p FROM Product AS p WHERE p.name =?1")
List<Product> findByName(String name);
// @Query 어노테이션과 @Param 어노테이션을 사용한 메서드
@Query("SELECT p FROM Product p WHERE p.name = :name")
List<Product> findByNameParam(@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);
8.6 QueryDSL 적용하기
- JPQL의 한계는 컴파일 시점에서 에러를 잡지 못하고 런타임 에러가 발생할 수 있다.
- QueryDSL은 문자열이 아니라 코드로 쿼리를 작성할 수 있도록 도와준다.
8.6.1 QueryDSL이란?
- QueryDSL1은 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크이다.
- 문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 플루언트(Fluent) API를 활용해 쿼리를 생성할 수 있다.
- 또한, @Query 어노테이션 방식은 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있는데, QueryDSL은 문자열이 아닌 코드로 쿼리를 작성하여 이를 해결할 수 있다.
8.6.2 QueryDSL의 장점
- IDE가 제공하는 코드 자동 완성 기능을 사용할 수 있다.
- 문법적으로 잘못된 쿼리를 허용하지 않는다. 따라서 정상적으로 활용된 QueryDSL은 문법 오류를 발생시키지 않는다.
- 고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있다.
- 코드로 작성하므로 가독성 및 생산성이 향상된다.
- 도메인 타입과 프로퍼티를 안전하게 참조할 수 있다.
8.6.3 QueryDSL을 사용하기 위한 프로젝트 설정
1) 사용을 위해서 의존성 주입, plugins에 추가가 필요하다.
2) Maven(complie), Gradle(other -> complieQuertdsl) 눌러 빌드해주면 작성했던 모든 엔티티 클래스의 Qdomain이라는 쿼리 타입의 클래스를 자체적으로 생성해서 메타데이터로 사용해서 SQL을 작성한다.
3) 리턴 타입
- List<T> fetch(): 조회 결과를 리스트로 반환
- T fetchOne(): 단 건의 조회 결과를 반환
- T fetchFirst(): 여러 건의 조회 결과 중 1건을 반환(내부 로직: .limit(1).fetchOne())
- Long fetchCount(): 조회 결과의 개수를 반환
- QueryResult<T> fetchResults(): 조회 결과 리스트와 개수를 포함한 QueryResults를 반환
8.6.4 기본적인 QueryDSL 사용하기
1) JPAQuery<Product> query = new JPAQuery(entityManager); (from 절부터 작성)
2) JPAQueryFactory query = new JPAQueryFactory(entityManager); (select절부터 작성 가능)
8.6.5 QuerydslPredicateExecutor, QuerydslRepositorySupport 활용
스프링 데이터 JPA에서는 QueryDSL을 더욱 편하게 사용할 수 있게 QuerydslPredicateExecutor 인터페이스와 QuerydslRepositorySupport 클래스를 제공한다.
- QuerydslPredicateExecutor 인터페이스 예시
1) Member 엔티티
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
// 생성자, getter, setter 등 생략...
}
2) MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long>, QuerydslPredicateExecutor<Member> {
}
MemberRepository 인터페이스는 JpaRepository와 QuerydslPredicateExecutor를 모두 상속받습니다. 다음으로, QuerydslRepositorySupport 클래스를 사용한 예제 코드.
- QuerydslRepositorySupport 클래스예시
1) CustomMemberRepository
public interface CustomMemberRepository {
List<Member> findByNameAndAge(String name, int age);
}
2) CustomMemberRepositoryImpl
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
public class CustomMemberRepositoryImpl extends QuerydslRepositorySupport implements CustomMemberRepository {
@PersistenceContext
private final EntityManager em;
private final JPAQueryFactory queryFactory;
public CustomMemberRepositoryImpl(EntityManager em) {
super(Member.class);
this.em = em;
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public List<Member> findByNameAndAge(String name, int age) {
QMember member = QMember.member;
return queryFactory
.selectFrom(member)
.where(nameEquals(name), ageEquals(age))
.fetch();
}
private BooleanExpression nameEquals(String name) {
return QMember.member.name.eq(name);
}
private BooleanExpression ageEquals(int age) {
return QMember.member.age.eq(age);
}
}
3) MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long>, CustomMemberRepository {
}
CustomMemberRepository 인터페이스를 정의하고, 구현 클래스인 CustomMemberRepositoryImpl에서 QuerydslRepositorySupport를 상속받아 사용했습니다. 마지막으로, MemberRepository에서 CustomMemberRepository를 상속받아 사용하면 된다.
8.7 [한걸음 더] JPA Auditing 적용
JPA Auditing은 JPA를 사용한 애플리케이션의 엔티티 변경 내역을 자동으로 추적하고 기록하는 기능이다. 이를 통해 엔티티 생성, 수정 날짜 및 수정자 등의 공통적인 정보를 쉽게 관리할 수 있다. JPA Auditing을 사용하기 위해 필요한 주요 구성 요소들은 다음과 같다.
1) `@EntityListeners(AuditingEntityListener.class)` : 엔티티에 JPA Auditing 리스너를 적용할 때 사용되는 어노테이션
2) `@CreatedBy, @LastModifiedBy` : 생성자와 수정자 정보를 자동으로 넣어주는 어노테이션
3) `@CreatedDate, @LastModifiedDate` : 생성시간 및 수정시간 정보를 자동으로 넣어주는 어노테이션
먼저, 스프링 부트 애플리케이션에서 JPA Auditing을 사용하려면 `@EnableJpaAuditing` 어노테이션을 메인 클래스에 추가해야 한다.
@SpringBootApplication
@EnableJpaAuditing
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
그리고 리스너를 사용하기 위해 추상 클래스를 생성한다. (예: `BaseTimeEntity`)
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
// getter 메서드 및 setter 메서드 생략
}
위에서 만든 추상 클래스를 상속받는 엔티티를 작성한다.
@Entity
public class Post extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
// getter 메서드 및 setter 메서드 생략
}
이제 JPA Auditing이 활성화되어, 엔티티가 생성되거나 수정될 때 마다 `createdDate`, `modifiedDate`에 자동으로 현재 시간이 기록된다.
8.8 정리
ORM의 개념과 자바 표준 ORM 기술 스펙인 JPA를 살펴봤다.
데이터를 다루는 영역은 애플리케이션 개발에 가장 중요한 부분이다.
대부분의 로직은 데이터를 가공, DB에 저장하거나 값을 효율적으로 가져오는 부분이 중요하다.
'북터디 > 스프링 부트 핵심 가이드' 카테고리의 다른 글
스프링 부트 핵심 가이드 9장 [연관관계 매핑] (0) | 2023.08.20 |
---|---|
스프링 부트 핵심 가이드 6장 [데이터베이스 연동] (0) | 2023.08.07 |
스프링 부트 핵심 가이드 5장 [API를 작성하는 다양한 방법] (0) | 2023.07.31 |
스프링 부트 핵심 가이드 4장 [스프링 부트 애플리케이션 개발하기] (0) | 2023.07.30 |
스프링 부트 핵심 가이드 3장 [개발 환경 구성] (0) | 2023.07.24 |