-
스프링 부트 핵심 가이드 4주차 : 8장책 공부/스프링 부트 핵심 가이드 2023. 12. 5. 00:01
나를 위한 모바일 가이드
Spring Data JPA 활용
8장.
8장
- JPQL
- 쿼리 메서드
- 정렬과 페이징 처리
- @Query
- QueryDSL
- JPA Auditing
Spring Data JPA에서 제공하는 기능들에 대해 더 알아보고 다양한 활용법에 대해 살펴본다.
그 과정에서 리포지토리 예제를 작성하고 , 리포지토리의 활용법을 테스트 코드를 통해 학습
실습 프로젝트는 새로 생성하지 않고 지난번에 했던 프로젝트에서 진행!
spring.io - Spring Data JPA Document
1. JPQL
JPA Query Language
JPA에서 사용할 수 있는 쿼리를 의미하며,SQL과 문법이 매우 비슷하다.
SQL과 JPQL의 차이점
-
SQL
테이블이나 컬럼의 이름을 사용. -
JPQL
매핑된 엔티티의 이름과 필드의 이름을 사용. 엔티티 객체를 대상으로 수행하는 쿼리이기 때문
2. 쿼리 메서드
JPA에서 제공하는 기본 메서드들은 식별자 기반으로 생성되기 때문에
별도의 메서드를 정의해서 사용하는 경우가 많다.
이때, 간단한 쿼리문을 작성하기 위해 사용되는 것이 쿼리 메서드!
- 생성
쿼리 메서드는 큰 범주로 분류해 보면
동작을 결정하는
주제 Subject
와서술어 Predicate
로 나눠볼 수 있다.메서드 구조
// 리턴타입 주제와서술어(속성) List<Person> findByLastnameAndEmail(String lastName, String email);
- 주제 키워드
주제 부분에 사용할 수 있는 주요 키워드는 다음과 같다.
조회
-
find...By
가장 일반적
으로 사용 조회 결과가 없으면null
반환-
get...By
단일 엔티티
만을 반환. 조회 결과가 없으면예외
던짐-
read...By
find...By
와 동일하게 동작-
query...By
find...By
와 동일하게 동작-
search...By
find...By
와 동일하게 동작-
stream...By
find...By
와 동일하게 동작반환 타입
을Stream
으로 지정하여 스트림 방식으로 결과를 처리할 수 있다.특정 데이터 존재 유무 확인
-
exists...By
특정 데이터 존재 확인
boolean
반환boolean existsByNumber(Long number);
#### 쿼리 결과 레코드 개수 반환 - #### count...By ```java long countByName(String name); ```
#### 삭제 쿼리 수행 - #### delete...By 리턴 타입 void ```java void deleteByNumber(Long number); ``` - #### remove...By 리턴 타입 long : 삭제 횟수 ```java long removeByName(String name); ```
#### 쿼리 결과의 개수를 제한 한번의 동작으로 여러건을 조회할 때 사용. 단건 조회시 <number> 생략 - #### ...First<number>By ```java List findFirst5ByName(String name); ``` - #### ...Top<number>By ```java List findTop10ByName(String name); ```- 서술부 : 조건자 키워드
Is
값의
일치
를 조건으로 사용하는 키워드생략되는 경우가 많으며, Equals 와 동일한 기능을 수행한다.
// findByNumber 메서드와 동일한 동작 Product findByNumberIs(Long number); Product findByNumberEquals(Long number);
(Is)Not
값의
불일치
를 조건으로 사용하는 키워드Is는 생략 가능
Product findByNumberIsNot(Long number); Product findByNumberNot(Long number);
(Is)Null , (Is)NotNull
값이
null
인지 검사하는 키워드List<Product> findByUpdatedAtNull(); List<Product> findByUpdatedAtIsNull(); List<Product> findByUpdatedAtNotNull(); List<Product> findByUpdatedAtIsNotNull();
(Is)True , (Is)False
데이터 타입이 boolean인 컬럼의 값을 확인하는 키워드
Product findByidActiveTrue(); Product findByidActiveIsTrue(); Product findByidActiveFalse(); Product findByidActiveIsFalse();
And , Or
여러 조건을 묶을 때 사용
Product findByNumberAndName(Long number, String name); Product findByNumberOrName(Long number, String name);
(Is)GreaterThan, (Is)LessThan, (Is)Between
숫자
나datetime
컬럼을 대상으로 한 비교 연산에 사용할 수 있는 키워드GreaterThan과 LessThan은 기본적으로 경곗값은 포함하지 않으며
경곗값
을포함
하려면Equal
키워드를 추가하면 된다.(Is)GreaterThan
: 비교 대상에 대한 초과의 개념으로 비교연산 수행(Is)LessThan
: 비교 대상에 대한 미만의 개념으로 비교연산 수행(Is)Between
: 비교 대상에 대해 범위를 적용하여 연산 수행// price보다 큰 값을 갖는 Price컬럼 조회 List<Product> findByPriceIsGreaterThan(Long price); List<Product> findByPriceGreaterThan(Long price); // price 값 이상인 Price 컬럼 조회 List<Product> findByPriceGreaterThanEqual(Long price); // price보다 작은 값을 갖는 Price 컬럼 조회 List<Product> findByPriceIsLessThan(Long price); List<Product> findByPriceIsLessThan(Long price); // price 값 이하인 Price 컬럼 조회 List<Product> findByPriceIsLessThanEqual(Long price); // lowPrice 이상 highPrice 이하인 Price 컬럼 조회 List<Product> findByPriceIsBetween(Long lowPrice, Long highPrice); List<Product> findByPriceBetween(Long lowPrice, Long highPrice);
StartsWith , EndsWith , Contains , (Is)Like
컬럼 값에서
일부 일치
여부를 확인하는 키워드SQL 쿼리에서
%
키워드와 동일한 역할을 한다.-
(Is)StartingWith
==StartsWith
문자열의앞
검색 -
(Is)EndingWith
==EndsWith
문자열의끝
검색 -
(Is)Containing
==Contains
문자열의양 끝
검색 -
(Is)Like
SQL의 Like 절 조건문에서와 마찬가지로 메서드에서 전달하는인자
에%
를명시
적으로 입력해주여야 한다.
// name컬럼 값이 '김'으로 시작하는 데이터 조회 List<Product> findByNameLike(String name); // String name = "김%" List<Product> findByNameIsLike(String name); // String name = "김%" // name 값이 name 컬럼에 포함돼있는 데이터 조회 List<Product> findByNameContains(String name); List<Product> findByNameContaining(String name); List<Product> findByNameIsContaining(String name); // name 값으로 시작하는 name 컬럼 데이터 조회 List<Product> findByNameStartsWith(String name); List<Product> findByNameStartingWith(String name); List<Product> findByNameIsStartingWith(String name); // name 값으로 끝나는 name 컬럼 데이터 조회 List<Product> findByNameEndsWith(String name); List<Product> findByNameEndingWith(String name); List<Product> findByNameIsEndingWith(String name);
3. 정렬과 페이징 처리
기본적인 정렬과 페이징 처리 방법에 대해 학습
1) 정렬 처리하기
- 쿼리 메서드 사용
단일 조건 정렬
일반적인 쿼리문에서 정렬을 사용할 때는 Order by 구문을 사용한다. 쿼리 메서드에서도 정렬 기능에 동일한 키워드가 사용된다.
// Asc : 오름차순 ㄱㄴㄷ , Desc : 내림차순 ㄷㄴㄱ List<Product> findByNameOrderByNumberAsc(String name); List<Product> findByNameOrderByNumberDesc(String name);
다중 조건 정렬
정렬을 위한 메서드를 작성할 때, 정렬 조건을 여러개로 나타내야 할 경우 And 나 Or 키워드를 사용하지 않고 우선순위를 기준으로 차례대로 작성해주면 된다.
List<Product> findByNameOrderByPriceAscStockDesc(String name);
다만, 이렇게 여러개의 조건을 메서드 이름으로 나열할 경우
가독성이 떨어지는 문제
가 생긴다.
- Sort : 정렬 조건을 매개변수로 받기
Sort
클래스를매개변수
로 전달하여 정렬을 처리할 수도 있다.List<Product> findByName(String name, Sort sort);
사용 예시1
productRepository .findByName( "펜", Sort.by( Sort.Order.asc("price"), Sort.Order.desc("stock") ) );
사용 예시2
@Test void sortingAndPagingTest() { ... productRepository.findByName("펜", getSort()) } private Sort getSort() { return Sort.by( Order.asc("price"), Order.asc("stock") ); }
2) 페이징 처리
페이징은
데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는것을 의미한다.
JPA에서는 페이징 처리를 위해
Page
와Pageable
을 사용한다.페이징 처리를 위한 쿼리 메서드 예시
Page<Product> findByName(String name, Pageable pageable);
리턴 타입으로
Page
를 설정하고, 매개변수에Pageable
타입의 객체를 정의한다.위의 예제를 사용하기 위해서는 아래와 같이 호출한다.
Page<Product> productPage = productRepository .findByName( "펜", PageRequest.of(0, 2) );
Pageable을 매개변수로 받으면, 해당 메서드의 반환 타입은 Page 객체가 된다.
PageRequest는 Pageable의 구현체이다.
_____________________🎈____________________
4. @Query
데이터베이스에서 값을 가져올 때는
앞 절에서 소개한 것처럼
메서드의 이름만으로 쿼리 메서드를 생성할 수도 있고
이번 절에서 살펴볼
@Query
어노테이션을 사용해직접 JPQL을 작성할 수도 있다.
데이터베이스를 다른 데이터베이스로 변경할 일이 없다면
직접 특화된 SQL을 작성할 수 있으며,
주로 튜닝된 쿼리를 사용하고자 할때
직접 SQL을 작성한다.- JPQL을 사용해 상품정보 조회 실습
Repository에 JPQL 이용 상품정보 조회 메서드 추가
WHERE 절의 파라미터에 매개변수 전달하는 방법 1
@Query("SELECT p FROM Product AS p WHERE p.name = ?1") List<Product> findByName(String name);
WHERE절에서 사용된
?1
은 첫 번째 파라미터에 매개변수를 전달받기 위해 사용된 것이다.이 방법은 순서가 꼬이기 쉽고, 그만큼 오류가 나기 쉽기 때문에 추천하지 않고
WHERE 절의 파라미터에 매개변수 전달하는 방법 2
@Param
어노테이션을 사용한 다음의 방법을 추천한다.@Query("SELECT p FROM Product AS p WHERE p.name = :name") List<Product> findByName(@Param("name") String name);
엔티티 타입이 아닌 원하는 컬럼 값만 추출해보기
@Query("SELECT p.name, p.price, p.stock FROM Product p WHERE p.name = :name") List<Object[]> findByNameParam(@Param("name") String name);
5. QueryDSL
메서드의 이름을 기반으로 생성하는 JPQL의 한계를
@Query
어노테이션을 통해 대부분 해소할 수 있지만직접 문자열을 입력하기 때문에,
컴파일 시점에 에러를 잡지 못하고
런타임 에러가 발생
할 수 있다.
이러한 이유로
개발환경에서는 문제가 없는 것처럼 보이다가
실제
운영
환경에 어플리케이션을배포하고 나서
오류가 발견
되는 리스크를 유발한다.
이와 같은 문제를 해결
하기 위해 사용되는 것이QueryDSL
이다.
QueryDSL은 문자열이 아니라 코드로 쿼리를 작성할 수 있도록 도와준다.
- QueryDSL 이란?
정적 타입
을 이용해SQL과 같은
쿼리
를생성
할 수 있도록 지원하는프레임워크
이다.
문자열이나 XML 파일을 통해 쿼리를 작성하는 대신
QueryDSL이 제공하는
플루언트 API
를 활용해쿼리를 생성할 수 있다.
- QueryDSL 장점
-
IDE가 제공하는
코드 자동 완성 기능
을 사용할 수 있다. -
문법적으로 잘못된 쿼리를 허용하지 않는다.
-
고정된 SQL 쿼리를 작성하지 않기 때문에
동적으로 쿼리
를 생성할 수 있다. -
코드로 작성하므로
가독성과 생산성이 향상
된다. -
도메인 타입과 프로퍼티를
안전하게 참조
할 수 있다.
- QueryDSL을 사용하기 위한 프로젝트 설정
gradle 설정
spring boot 2.7.12 기준
1. 의존성 추가
Spring Boot 2.6 이상 버전에서는 Querydsl 5.0을 사용한다. querydsl-jpa querydsl-apt
2. plugin 추가
plugins { ... id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" }
3. 관련 설정 추가
// -------- queryDSL 설정 추가 ------------------- // querydsl에서 사용할 경로 설정 def querydslDir = "$buildDir/generated/querydsl" // JPA 사용 여부와 사용할 경로를 설정 querydsl { jpa = true querydslSourcesDir = querydslDir } // build 시 사용할 sourceSet 추가 sourceSets { main.java.srcDir querydslDir } // querydsl 컴파일시 사용할 옵션 설정 compileQuerydsl{ options.annotationProcessorPath = configurations.querydsl } // querydsl 이 compileClassPath 를 상속하도록 설정 configurations { compileOnly { extendsFrom annotationProcessor } querydsl.extendsFrom compileClasspath }
4. gradle build 후 어플리케이션 실행
gradle build 후 어플리케이션을 실행하면 Q도메인 파일이 생성된다.
나는 이 프로젝트에서 사용한 도메인 이름이 Product라서 파일 이름이
QProduct
라고 생겼다.
Q도메인 파일은 아래 위치에 있다. jpaStudy - build - generated - querydsl - proj.package.path.data.entity - QProduct.java
- 기본적인 QueryDSL 사용하기
1. 테스트 코드를 통해 QueryDSL의 동작을 확인
1-1. 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("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()); } }
1-2. JPAQueryFactory 사용으로 전체 컬럼에 대해 조건 처리 후 가져오기
@PersistenceContext EntityManager entityManager; @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("=========================================="); } }
1-3. JPAQueryFactory 사용으로 일부 컬럼에 대해 조건 처리 후 가져오기
@PersistenceContext EntityManager entityManager; @Test void queryDslTest3() { JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager); QProduct qProduct = QProduct.product; // Select 대상이 1개인 경우 List<String> productList = jpaQueryFactory .select(qProduct.name) .from(qProduct) .where(qProduct.name.eq("펜")) .orderBy(qProduct.price.asc()) .fetch(); productList.forEach(productName -> { System.out.println("============================="); System.out.println("productName : " + productName); System.out.println("============================="); }); // Select 대상이 여러개인 경우 List<Tuple> tupleList = jpaQueryFactory .select(qProduct.name, qProduct.price) .from(qProduct) .where(qProduct.name.eq("펜")) .orderBy(qProduct.price.asc()) .fetch(); tupleList.forEach(product -> { System.out.println("============================="); System.out.println("productName : " + product.get(qProduct.name)); System.out.println("productPrice : " + product.get(qProduct.price)); System.out.println("============================="); }); }
2. 실제 비즈니스 로직에서 활용
JPAQueryFactory 객체를 Bean으로 등록하여
필요시 객체를 Spring 에서 주입하도록 설정
2-1. config 클래스 생성
@Configuration public class QueryDSLConfiguration { @PersistenceContext EntityManager entityManager; @Bean public JPAQueryFactory jpaQueryFactory() { return new JPAQueryFactory(entityManager); } }
2-2. TEST
JPAQueryFactory를 Spring에서 주입해줌으로써 EntityManager 와 JPAQueryFactory 초기화 과정이 필요없어졌다.
@Autowired JPAQueryFactory jpaQueryFactory; @Test void queryDslTest3() { QProduct qProduct = QProduct.product; List<String> productList = jpaQueryFactory .select(qProduct.name) .from(qProduct) .where(qProduct.name.eq("펜")) .orderBy(qProduct.price.asc()) .fetch(); productList.forEach(productName -> { System.out.println("============================="); System.out.println("productName : " + productName); System.out.println("============================="); }); }
QuerydslRepositorySupport 활용
- QuerydslPredicateExecutor ,Spring Data JPA 에서는
QueryDSL을 더욱 편하게 사용할 수 있게
QuerydslPredicateExcutor 인터페이스와
QuerydslRepositorySupport 클래스를 제공한다.
1. QuerydslPredicateExcutor 인터페이스
QuerydslPredicateExcutor는
JpaRepository와 함께
리포지토리에서 QueryDSL을 사용할 수 있게 인터페이스를 제공한다.
QuerydslPredicateExcutor 인터페이스를 활용하면
더욱 편하게 QueryDSL을 사용할 수 있지만
join
,fetch
사용 불가
실습!
1-1. QProductRepository 생성
public interface QProductRepository extends JpaRepository<Product, Long> , QuerydslPredicateExecutor<Product> { }
1-2. Test 코드 실습
@SpringBootTest class QProductRepositoryTest { @Autowired QProductRepository qProductRepository; @Test void queryDSLTest2() { QProduct qProduct = QProduct.product; Iterable<Product> productList = qProductRepository.findAll( qProduct.name.contains("펜") .and(qProduct.price.between(550, 1500)) ); productList.forEach(product -> { System.out.println(product.getNumber()); System.out.println(product.getStock()); }); } @Test public void queryDSLTest1() { Predicate predicate = QProduct.product.name.containsIgnoreCase("펜") .and(QProduct.product.price.between(1000, 2500)); Optional<Product> foundProduct = qProductRepository.findOne(predicate); if (foundProduct.isPresent()) { Product product = foundProduct.get(); System.out.println(product.getNumber()); System.out.println(product.getName()); } } }
QuerydslRepositorySupport 추상 클래스 사용하기
2.QuerydslRepositorySupport 클래스 역시
QueryDSL 라이브러리를 사용하는데 유용한 기능을 제공한다.
가장 보편적인 사용방식은
CustomRepository를 활용해 리포지토리를 구현하는 방식이다.
간단하게 구조를 설명하자면 다음과 같다.
-
먼저 앞에서 사용했던 방식처럼
JpaRepository를 상속받는
ProductRepository
를생성
한다. -
이때 직접 구현한 쿼리를 사용하기 위해서는
JpaRepository를 상속받지 않는
리포지토리인터페이스
인ProductRepositoryCustom
을생성
한다. 이 인터페이스에 정의하고자 하는 기능들을 정의! -
ProductRepositoryCustom에서 정의한 메서드를 사용하기 위해
ProductRepository에서 ProductRepositoryCustom을 상속
받는다. -
ProductRepositoryCustom에서 정의된 메서드를 기반으로 실제 쿼리 작성을 하기 위해 구현체인
ProductRepositoryCustomImpl 클래스를 생성
한다. -
ProductRepositoryCustomImple 클래스에서
는 다양한 방법으로 쿼리를 구현할 수 있지만 QueryDSL을 사용하기 위해QueryDslRepositorySupport를 상속받는다.
위와 같이 구성하면
DAO나 서비스에서
리포지토리에 접근하기 위해 ProductRepository를 사용한다.
ProductRepository를 활용함으로써 QueryDSL의 기능도 사용할 수 있게 된다.
실습!
2-1. ProductRepository , ProductRepositoryCustom , ProductRepositoryCustomImpl 생성
이전에 만들어둔 인터페이스 이름과 겹치지 않게
support 패키지 생성 후 이 안에 구현.
2-1-1. ProductRepositoryCustom
public interface ProductRepositoryCustom { List<Product> findByName(String name); }
2-1-2. ProductRepositoryCustomImpl
@Component public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport implements ProductRepositoryCustom { // QuerydslRepositorySupport 상속시 필수 구현 부분 // 생성자를 통해 도메인 클래스를 부모 클래스에 전달해야 한다. public ProductRepositoryCustomImpl() { super(Product.class); } @Override public List<Product> findByName(String name) { QProduct product = QProduct.product; return from(product) // QuerydslRepositorySupport 제공 메서드. JPAQuery를 반환한다. .where(product.name.eq(name)) // 이하 QueryDSL(JPAQuery) 메서드 .select(product) .fetch(); } }
2-1-3. ProductRepository
@Repository("productRepositorySupport") // 다른 파일과 같은 이름으로, bean 충돌 방지를 위해 bean 이름 지정 public interface ProductRepository extends JpaRepository<Product, Long> , ProductRepositoryCustom { }
2-2. Test 코드 실습
리포지토리를 생성하면서
모든 로직을 구현했기 때문에
findByName() 메서드를 사용할 때는
간단히 구현해서 사용할 수 있다.
@SpringBootTest class ProductRepositoryTest { @Autowired ProductRepository productRepository; @Test void findByNameTest() { List<Product> productList = productRepository.findByName("펜"); productList.forEach(product -> { System.out.println(product.getNumber()); System.out.println(product.getName()); System.out.println(product.getPrice()); System.out.println(product.getStock()); }); } }
6. JPA Auditing
JPA에서
Auditing
이란감시하다
라는 뜻으로각 데이터마다
누가
,언제
데이터를 생성했고 변경했는지감시한다는 의미로 사용됨
엔티티 클래스에는 공통적으로 들어가는 필드가 있다.
예를 들면,
생성 일자
와변경 일자
가 있다.
이런 필드들은
매번 생성하가나 변경할 때 마다 값을 주입해야 하는 번거로움이 있다.
이런 번거로움을 해결하기 위해 JPA Auditing을 사용한다.
- JPA Auditing 기능 활성화
스프링 부트 애플리케이션에 Auditing 기능을 활성화해야 한다.
Config 파일을 새로 만든 후
@EnableJpaAuditing
어노테이션을 추가해주면 된다.@Configuration @EnableJpaAuditing public class JpaAuditingConfiguration { }
- BaseEntity 만들기
각 엔티티에 공통으로 들어가게 되는 컬럼을 하나의 클래스로 분리하고
@MappedSuperclass
: JPA 엔티티 클래스가 상속받을 경우 자식 클래스에게 매핑 정보를 전달한다.@EntityListeners(AuditingEntityListener.class)
: 엔티티를 데이터베이스에 적용하기 전후로 콜백을 요청할 수 있게 한다.위 2개의 어노테이션은 꼭 달아주어야 한다.
@Getter @Setter @ToString @SuperBuilder @NoArgsConstructor @AllArgsConstructor @MappedSuperclass // JPA 엔티티 클래스가 상속받을 경우 자식 클래스에게 매핑 정보를 전달한다. @EntityListeners(AuditingEntityListener.class) // 엔티티를 데이터베이스에 적용하기 전후로 콜백을 요청할 수 있게 한다. public class BaseEntity { @CreatedDate // 데이터 생성 날짜 자동 주입 @Column(updatable = false) private LocalDateTime createdAt; @LastModifiedDate // 데이터 수정 날짜 자동 주입 private LocalDateTime updatedAt; }
엔티티 파일에 BaseEntity 상속 및 코드 정리
@EqualsAndHashCode @ToString
위의 두 어노테이션은 callSuper 옵션을 true로 설정해주고,
@Builder 어노테이션은 @SuperBuilder 어노테이션으로 변경해준다.
그리고 BaseEntity를 상속받는다.
@EqualsAndHashCode(callSuper = true) // callSuper = true : 부모 클래스 필드 포함 @ToString(callSuper = true) // callSuper = true : 부모 클래스 필드 포함 @Getter @Setter @SuperBuilder @NoArgsConstructor @AllArgsConstructor @Entity @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; }
- Test
@Autowired ProductRepository productRepository; @Test void auditingTest() { Product product = Product.builder() .name("펜") .price(1000) .stock(100) .build(); Product save = productRepository.save(product); System.out.println("product name : " + save.getName()); System.out.println("createdAt : " + save.getCreatedAt()); }
테스트 통과 확인
'책 공부 > 스프링 부트 핵심 가이드' 카테고리의 다른 글
스프링 부트 핵심 가이드 6주차: 10장 (0) 2023.12.06 스프링 부트 핵심 가이드 5주차: 9장 (0) 2023.12.06 스프링 부트 핵심 가이드 3주차 : 6장 (0) 2023.12.04 스프링 부트 핵심 가이드 2주차 : 4~5장 (0) 2023.12.02 스프링 부트 핵심 가이드 1주차 : 개발 준비 학습 (2) 2023.12.02